🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

介紹

Lingo.dev ,我編寫了大量 TypeScript 程式碼。我當然不算是高手,但我確實嘗試過一些超越基本類型的功能。

這篇文章描述了許多功能(以及您可能想要使用它們的時間),以幫助您在絕對基礎知識之外擴展您的知識。

  1. 只讀陣列、元組和as const斷言

預設情況下,陣列和物件是可變的,而 TypeScript 會將字面值擴展為其通用類型。這使得 TypeScript 更難幫助您捕捉錯誤並提供準確的自動完成功能。

const colors = ["red", "green", "blue"];
// Type: string[] - could be any strings
colors.push("yellow"); // Allowed, might not be what you want

type Color = (typeof colors)[number]; // string (too general!)

解決方案

用作as const使所有內容只讀並保留文字類型,或對特定陣列使用readonly

const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
colors.push("yellow"); // ✗ Error: can't modify readonly array

type Color = (typeof colors)[number]; // "red" | "green" | "blue" ✓

// Or for function parameters:
function display(items: readonly string[]) {
  items.push("x"); // ✗ Error: can't modify
  items.forEach(console.log); // ✓ OK: reading is fine
}

何時使用

  • 不應改變的配置或常數資料

  • 防止意外突變

  • 保留文字類型以實現更好的類型推斷

  • 不應修改的函數參數

了解更多: TypeScript 文件:ReadonlyArray

  1. keyof typeof用於 object-as-const 枚舉

TypeScript 枚舉有一些特殊之處,會產生 JavaScript 程式碼。有時你只想在物件中定義常數,並從中派生類型。

解決方案

組合as const (鎖定文字值)、 typeof (取得物件的類型)和keyof (取得鍵或值的並集)。

// Define your constants as a plain object
const STATUS = {
  PENDING: "pending",
  APPROVED: "approved",
  REJECTED: "rejected",
} as const; // Lock in the literal values

// Get a union of the values
type Status = (typeof STATUS)[keyof typeof STATUS];
// "pending" | "approved" | "rejected"

function setStatus(status: Status) {
  // TypeScript validates and autocompletes!
}

setStatus(STATUS.APPROVED); // ✓
setStatus("pending"); // ✓
setStatus("invalid"); // ✗ Error

何時使用

  • 具有更好 JavaScript 輸出的枚舉替代方案

  • 使用派生類型建立基於 const 的配置

  • 當你需要執行時值和編譯時類型時

了解更多: TypeScript 文件:typeof 類型

  1. 帶標籤的元組元素

[number, number, boolean]這樣的元組可以工作,但每個位置的含義並不明顯。是[width, height, visible]還是[x, y, enabled]

解決方案

為元組位置賦予有意義的名稱,這些名稱會顯示在編輯器的自動完成和錯誤訊息中。

// Before: unclear what each number means
type Range = [number, number, boolean?];

// After: self-documenting
type Range = [start: number, end: number, inclusive?: boolean];

function createRange([start, end, inclusive = false]: Range) {
  // Your editor will show you the parameter names!
  return { start, end, inclusive };
}

createRange([1, 10, true]); // Clear what each argument means

何時使用

  • 基於元組的函數參數

  • 傳回具有多個相關資料的值

  • 任何位置意義不明顯的元組

了解更多: TypeScript 文件:元組類型

  1. 索引存取和元素類型提取

您有一個複雜的類型,並且只想引用一個屬性的類型,或者提取陣列中的內容,而無需重複。

解決方案

使用括號表示法( Type["property"] )存取屬性類型,使用[number]取得陣列元素類型。

type User = {
  id: number;
  profile: {
    name: string;
    emails: string[];
  };
};

// Access nested property types
type ProfileType = User["profile"]; // { name: string; emails: string[] }
type NameType = User["profile"]["name"]; // string

// Extract array element type
type Email = User["profile"]["emails"][number]; // string

何時使用

  • 從現有型別衍生型別(DRY 原則)

  • 提取陣列/元組元素類型

  • 使用嵌套結構而無需重新定義類型

了解更多: TypeScript 文件:索引存取類型

  1. 使用者定義的類型保護( arg is T

您編寫了一個函數來檢查某物是否屬於某種類型,但 TypeScript 並不理解該檢查實際上會縮小類型。

function isPerson(x: unknown) {
  return typeof x === "object" && x !== null && "name" in x;
}

function greet(x: unknown) {
  if (isPerson(x)) {
    x.name; // ✗ Error: TypeScript still thinks x is 'unknown'
  }
}

解決方案

使用類型謂詞( arg is Type )告訴 TypeScript 你的函數執行類型檢查。

type Person = { name: string; age: number };

function isPerson(x: unknown): x is Person {
  return (
    typeof x === "object" &&
    x !== null &&
    "name" in x &&
    typeof (x as any).name === "string"
  );
}

function greet(x: unknown) {
  if (isPerson(x)) {
    console.log(x.name); // ✓ TypeScript knows x is Person here!
  }
}

何時使用

  • 驗證來自 API 或使用者輸入的資料

  • 類型安全驗證函數

  • 區分聯合中的類型

了解更多: TypeScript 文件:類型謂詞

  1. 徹底檢查, never

你有一個聯合類型(例如不同的形狀或狀態)和一個 switch 語句。後來,有人向聯合中加入了一個新的變數,但忘記在 switch 中處理它了。沒有拋出任何錯誤——它只是默默地不起作用。

解決方案

新增一個default情況,將值賦給never類型。如果所有情況都已處理,則無法存取預設情況。如果缺少一個情況,TypeScript 會報錯,因為該值無法賦給never類型。

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If all cases are handled, this is unreachable
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

// Later, someone adds triangle:
// type Shape = ... | { kind: "triangle"; base: number; height: number };
// ✓ TypeScript error in default case: triangle is not assignable to never!

何時使用

  • 針對可區分聯合的 Switch 語句

  • 確保處理所有聯合變體

  • 當類型隨時間演變時捕獲錯誤

了解更多: TypeScript 文件:詳盡性檢查

  1. 僅限匯入和匯出類型( import type / export type

有時你會從其他模組導入類型,但這些導入的內容即使只是用於類型檢查,也會顯示在編譯後的 JavaScript 檔案中。這可能會導致循環依賴或包膨脹。

解決方案

使用import type告訴 TypeScript:“這只是為了類型檢查,將其從 JavaScript 中完全刪除。”

// Regular import - might end up in compiled JS
import { User } from "./types";

// Type-only import - guaranteed to be removed from JS
import type { User } from "./types";

// Mixed imports
import { saveUser, type User } from "./api";
//       ^^^^^^^^^  ^^^^^^^^^^^
//       value      type-only

何時使用

  • 防止循環依賴問題

  • 保持 JavaScript 包更小

  • 當使用需要明確類型匯入( isolatedModules )的建置工具時

  • 澄清意圖(這僅適用於類型,不適用於執行時程式碼)

了解更多: TypeScript 文件:導入類型

  1. 非程式碼資產的環境模組聲明

您匯入非 TypeScript 檔案(如映像、CSS 或資料檔案),但 TypeScript 不知道它們應該具有哪種類型。

import logo from "./logo.svg"; // ✗ Error: Cannot find module

解決方案

建立環境模組聲明,告訴 TypeScript 如何輸入這些導入。

// In a .d.ts file (like global.d.ts or declarations.d.ts)
declare module "*.svg" {
  const url: string;
  export default url;
}

declare module "*.css" {
  const classes: { [key: string]: string };
  export default classes;
}

// Now these work:
import logo from "./logo.svg"; // logo: string
import styles from "./app.css"; // styles: { [key: string]: string }

何時使用

  • 輸入圖像、字體、樣式的匯入

  • 您的建置工具無法處理的 JSON 或資料文件

  • 捆綁器處理的任何非 TypeScript 資產

了解更多:TypeScript 文件:模組聲明模板

  1. satisfies運算符

有時您希望 TypeScript 檢查物件是否與類型匹配,但您希望 TypeScript 記住您使用的特定值(而不僅僅是它們是字串或數字)。

// Without satisfies - loses specific information
const routes: Record<string, string> = {
  home: "/",
  profile: "/users/:id",
};
// routes.profile is just 'string', not the specific "/users/:id"

解決方案

satisfies檢查物件是否符合某種類型,而不改變 TypeScript 記住的內容。

const routes = {
  home: "/",
  profile: "/users/:id",
} satisfies Record<string, `/${string}`>; // Must be strings starting with "/"

// routes.profile is still the literal "/users/:id" - exact value preserved!

何時使用

  • 需要驗證和特定值類型的配置物件

  • 當您需要自動完成精確值時,而不僅僅是一般類型

了解更多: TypeScript 文件:滿足運算符

  1. 斷言函數( assertsasserts x is T

有時,您需要一個函數,當條件不滿足時拋出錯誤。類型保護(上文)僅適用於if語句 - 它們不會影響函數呼叫後的程式碼。

function assertNotNull(x: unknown) {
  if (x == null) throw new Error("Value is null!");
}

const data: string | null = getValue();
assertNotNull(data);
// TypeScript still thinks data might be null here

解決方案

斷言函數使用asserts來告訴 TypeScript:“如果此函數返回(不拋出),則條件為真。”

function assertNotNull<T>(x: T): asserts x is NonNullable<T> {
  if (x == null) throw new Error("Value is null!");
}

const data: string | null = getValue();
assertNotNull(data);
// ✓ TypeScript now knows data is definitely string here!
data.toUpperCase(); // Safe to use

何時使用

  • 失敗時拋出的驗證函數

  • 強制執行執行時不變數

  • 在函數邊界進行早期錯誤檢查

了解更多: TypeScript 文件:斷言函數

  1. 字串模式的模板字面量類型

假設您有諸如"user:login""user:logout""post:create"等事件名稱。您希望 TypeScript 自動完成這些事件並擷取拼字錯誤,但手動列出這些事件名稱實在太多了。

解決方案

模板文字類型可讓您使用與 JavaScript 模板字串相同的語法來描述字串模式。

// Generate all combinations automatically
type EventName = `${"user" | "post"}:${"create" | "delete"}`;
// Result: "user:create" | "user:delete" | "post:create" | "post:delete"

function trackEvent(event: EventName) {
  // TypeScript will autocomplete and validate the event names!
}

trackEvent("user:create"); // ✓ OK
trackEvent("user:update"); // ✗ Error - not a valid combination

何時使用

  • 遵循某種模式的 API 路由或事件名稱

  • 帶有前綴/後綴的 CSS 類別名

  • 任何結構化字串格式(如資料庫表名、檔案路徑)

了解更多: TypeScript 文件:範本文字類型

  1. 分配條件類型

您想要透過對每個成員套用邏輯來過濾或轉換聯合類型(如string | number | null )。

解決方案

當檢查的類型是「裸露的」(未包裝在另一種類型中)時,條件類型會自動分佈在聯合上。

// Remove null and undefined from a union
type NonNullish<T> = T extends null | undefined ? never : T;

// This distributes: checks each member separately
type Clean = NonNullish<string | number | null>;
// string | number (null was filtered out)

// Extract only function types
type FunctionsOnly<T> = T extends (...args: any[]) => any ? T : never;
type Fns = FunctionsOnly<string | ((x: number) => void) | boolean>;
// (x: number) => void

何時使用

  • 過濾聯合類型

  • 建立實用程式類型,例如ExcludeExtract

  • 以不同的方式改變工會的每個成員

了解更多: TypeScript 文件:分配條件類型

  1. infer條件語句中的類型

您需要提取一段複雜類型(例如「這個函數回傳什麼類型?」或「這個陣列裡面有什麼?」)。

解決方案

使用infer建立一個類型變數,捕獲您正在檢查的類型的一部分。

// Extract the return type of a function
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;

type MyFunc = (x: number) => string;
type Result = ReturnType<MyFunc>; // string

// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;

type Numbers = ElementType<number[]>; // number
type Mixed = ElementType<(string | boolean)[]>; // string | boolean

何時使用

  • 從函數中提取參數或返回類型

  • 從陣列或元組取得元素類型

  • 解析模板文字或複雜結構中的類型

了解更多: TypeScript 文件:在條件類型內推斷

  1. 映射型別修飾符( +readonly-?等)

有時你需要將現有型別的所有屬性設為必要(移除? ),或將所有內容設為可變(移除readonly )。手動重寫每個屬性非常繁瑣。

解決方案

映射類型可以新增( + )或刪除( -readonly和可選( ? )修飾符。

// Remove readonly from all properties
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove optional (?) from all properties
type Required<T> = {
  [K in keyof T]-?: T[K];
};

type Config = Readonly<{ port?: number; host?: string }>;
type EditableConfig = Mutable<Required<Config>>; // { port: number; host: string }

何時使用

  • 建立唯讀類型的可編輯版本

  • 使所有屬性成為驗證函數所必需的

  • 建構轉換屬性修飾符的實用程式類型

了解更多: TypeScript 文件:映射修飾符

  1. 映射類型中的鍵重映射( as

您想要透過變更屬性名稱(例如刪除前綴或過濾掉某些屬性)來轉換物件類型,但映射類型通常會保留相同的鍵。

解決方案

在映射類型中使用as來在迭代鍵時轉換鍵。

// Remove private properties (those starting with _)
type RemovePrivate<T> = {
  [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

type WithPrivate = { name: string; _secret: number };
type Public = RemovePrivate<WithPrivate>; // { name: string }

// Add a prefix to all keys
type Prefixed<T> = {
  [K in keyof T as `app_${string & K}`]: T[K];
};

type Original = { id: number; name: string };
type Result = Prefixed<Original>; // { app_id: number; app_name: string }

何時使用

  • 透過過濾內部屬性來建立類型的公共版本

  • 命名約定之間的轉換(camelCase 到 snake_case)

  • 根據關鍵模式過濾物件類型

了解更多: TypeScript 文件:映射類型中的鍵重新映射

16.Const型別參數

當您將陣列傳遞給泛型函數時,TypeScript 通常會將其「擴展」為通用陣列類型,從而丟失有關特定值的資訊。

function identity<T>(value: T) {
  return value;
}

const pair = identity([1, 2]); // Type is number[], not [1, 2]

解決方案

在類型參數前加入const來告訴 TypeScript:“盡可能保持具體。”

function identity<const T>(value: T) {
  return value;
}

const pair = identity([1, 2]); // Type is [1, 2] - exact tuple preserved!

何時使用

  • 應保留精確陣列/元組結構的函數

  • 想要透過轉換來追蹤文字值的建構器函數

了解更多: TypeScript 文件:const 類型參數

  1. 可變元組類型和展開

如何輸入一個需要接受不同數量參數的函數,同時追蹤每個參數的類型?

解決方案

可變元組可讓您處理可增長或收縮的類型清單。可以把...想像成「將這個類型清單展開到這裡」。

// Type that adds an element to the end of a tuple
type Push<T extends unknown[], U> = [...T, U];

type Result = Push<[string, number], boolean>; // [string, number, boolean]

// Real example: Typing a function wrapper
function logged<Args extends unknown[], Return>(
  fn: (...args: Args) => Return,
): (...args: Args) => Return {
  return (...args) => {
    console.log("Calling with:", args);
    return fn(...args);
  };
}

何時使用

  • 包裝函數並保留其確切的參數類型

  • 建立類型安全的函陣列合實用程式

  • 建構元組操作類型

了解更多: TypeScript 文件:元組類型

  1. 函數中的this參數

當函數使用this時,TypeScript 不知道this應該是什麼類型。這會導致方法、回呼和事件處理程序出現問題。

function setName(name: string) {
  this.name = name; // ✗ Error: 'this' has type 'any'
}

解決方案

新增一個明確的this參數(在執行時不算作真正的參數)來鍵入this應該是什麼。

interface Model {
  name: string;
  setName(this: Model, newName: string): void;
}

const model: Model = {
  name: "Initial",
  setName(this: Model, newName: string) {
    this.name = newName; // ✓ TypeScript knows what 'this' is!
  },
};

model.setName("Updated"); // Works
const fn = model.setName;
fn("Test"); // ✗ Error: 'this' context is wrong

何時使用

  • 依賴於此this方法

  • 事件處理程序回調

  • 設計為使用.call().apply()呼叫的函數

了解更多: TypeScript 文件:this 參數

  1. 名詞類打字的unique symbol

TypeScript 使用「結構類型」—兩個具有相同結構的類型被視為相同。有時,您需要結構相同但邏輯不同的類型(例如 UserID 與 ProductID,它們都是字串)。

type UserId = string;
type ProductId = string;

function getUser(id: UserId) {
  /* ... */
}

const productId: ProductId = "prod-123";
getUser(productId); // ✗ We want this to be an error, but it's not!

解決方案

使用unique symbol來建立一個“品牌”,即使結構相同,也會使類型不相容。

declare const USER_ID: unique symbol;
type UserId = string & { [USER_ID]: true };

declare const PRODUCT_ID: unique symbol;
type ProductId = string & { [PRODUCT_ID]: true };

function getUser(id: UserId) {
  /* ... */
}

const productId = "prod-123" as ProductId;
getUser(productId); // ✓ Now this IS an error!

何時使用

  • 防止混淆不同類型的 ID(使用者 ID、訂單 ID 等)

  • 建立不能被意外替換的「名義」類型

  • 用於註冊表或依賴注入的類型安全性金鑰

了解更多: TypeScript 文件:唯一符號

  1. 模組擴充與聲明合併

您正在使用第三方程式庫並需要在其類型中新增屬性(例如新增自訂配置選項),但您無法編輯程式庫的程式碼。

解決方案

使用模組增強從外部加入現有介面或模組。

// In your own .d.ts file
declare module "express" {
  // Add to Express's Request interface
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now TypeScript knows about req.user in your Express handlers!

何時使用

  • 使用自訂屬性擴充庫類型

  • 為非完全類型的庫功能新增類型

  • 您正在註冊新功能的插件系統

了解更多:TypeScript 文件:模組擴充

  1. 建構函式簽章與抽象「可新建」型

你想寫一個函數,它接受一個類別(而不是實例)並建立它的實例。如何輸入「可以用new構造的東西」?

function createInstance(SomeClass: ???) {
  return new SomeClass();
}

解決方案

使用建構子簽章: new (...args: any[]) => T來描述可以建構的東西。

// Type describing a constructor
type Constructor<T = unknown, Args extends unknown[] = any[]> = new (
  ...args: Args
) => T;

function createInstance<T>(Ctor: Constructor<T>): T {
  return new Ctor();
}

class User {
  name = "Unknown";
}

const user = createInstance(User); // user: User ✓

// More complex: factory with specific constructor arguments
function createPair<T>(
  Ctor: Constructor<T, [string, number]>,
  name: string,
  age: number,
): T {
  return new Ctor(name, age);
}

何時使用

  • 依賴注入框架

  • 建立實例的工廠函數

  • 將類別作為值使用的通用程式碼

  • 測試模擬建構函數的實用程序

了解更多: TypeScript 文件:介面中的建構子簽名


原文出處:https://dev.to/lingodotdev/beyond-the-basics-21-typescript-features-you-might-not-know-about-1dbn


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝27   💬3   ❤️7
660
🥈
我愛JS
📝1   💬9   ❤️1
72
🥉
御魂
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付