應對 TypeScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

23 類型運算概觀



在本章中,我們將探討如何在 TypeScript 中於編譯時使用類型運算。

請注意,本章的重點在於學習如何使用類型運算。因此,我們將大量使用文字類型,而範例與實際應用較無關。

23.1 類型作為元值

考慮以下兩個層級的 TypeScript 程式碼

類型層級是程式層級的元層級。

層級 可用於 運算元 運算
程式層級 執行時 函式
類型層級 編譯時 特定類型 泛型類型

我們可以進行類型運算,這代表什麼意思?以下程式碼是一個範例

type ObjectLiteralType = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)

在 A 行中,我們執行以下步驟

在類型層級,我們可以使用下列「值」進行運算

type ObjectLiteralType = {
  prop1: string,
  prop2: number,
};

interface InterfaceType {
  prop1: string;
  prop2: number;
}

type TupleType = [boolean, bigint];

//::::: Nullish types and literal types :::::
// Same syntax as values, but they are all types!

type UndefinedType = undefined;
type NullType = null;

type BooleanLiteralType = true;
type NumberLiteralType = 12.34;
type BigIntLiteralType = 1234n;
type StringLiteralType = 'abc';

23.2 泛型類型:類型的工廠

泛型類型是元層級的函式,例如

type Wrap<T> = [T];

泛型類型 Wrap<> 具有參數 T。其結果為 T,包裝在一個元組類型中。以下是我們如何使用這個元函數

// %inferred-type: [string]
type Wrapped = Wrap<string>;

我們將參數 string 傳遞給 Wrap<>,並將結果給予別名 Wrapped。結果是一個具有單一元件的元組類型,也就是類型 string

23.3 聯合類型和交集類型

23.3.1 聯合類型 (|)

類型運算子 | 用於建立聯合類型

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "a" | "b" | "c" | "d"
type Union = A | B;

如果我們將類型 A 和類型 B 視為集合,則 A | B 是這些集合的集合論聯集。換句話說:結果的成員至少是其中一個運算元的成員。

在語法上,我們也可以將 | 放在聯合類型第一個元件的前面。當類型定義跨越多行時,這會很方便

type A =
  | 'a'
  | 'b'
  | 'c'
;
23.3.1.1 聯合作為元值的集合

TypeScript 將元值的集合表示為文字類型的聯合。我們已經看過一個範例

type Obj = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof Obj;

我們很快就會看到用於迴圈處理此類集合的類型層級運算。

23.3.1.2 物件類型的聯合

由於聯合類型中的每個成員都是至少一個元件類型的成員,因此我們只能安全地存取所有元件類型共有的屬性 (A 行)。若要存取任何其他屬性,我們需要類型防護 (B 行)

type ObjectTypeA = {
  propA: bigint,
  sharedProp: string,
}
type ObjectTypeB = {
  propB: boolean,
  sharedProp: string,
}

type Union = ObjectTypeA | ObjectTypeB;

function func(arg: Union) {
  // string
  arg.sharedProp; // (A) OK
  // @ts-expect-error: Property 'propB' does not exist on type 'Union'.
  arg.propB; // error

  if ('propB' in arg) { // (B) type guard
    // ObjectTypeB
    arg;

    // boolean
    arg.propB;
  }
}

23.3.2 交集類型 (&)

類型運算子 & 用於建立交集類型

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "b" | "c"
type Intersection = A & B;

如果我們將類型 A 和類型 B 視為集合,則 A & B 是這些集合的集合論交集。換句話說:結果的成員是兩個運算元的成員。

23.3.2.1 物件類型的交集

兩個物件類型的交集具有兩種類型的屬性

type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
type Both = {
  prop1: boolean,
  prop2: number,
};

// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;

(泛型類型 IsAssignableTo<> 會在稍後說明。)

23.3.2.2 使用交集類型進行混入

如果我們將物件類型 Named 混入另一個類型 Obj 中,則我們需要一個交集類型 (A 行)

interface Named {
  name: string;
}
function addName<Obj extends object>(obj: Obj, name: string)
  : Obj & Named // (A)
{
  const namedObj = obj as (Obj & Named);
  namedObj.name = name;
  return namedObj;
}

const obj = {
  last: 'Doe',
};

// %inferred-type: { last: string; } & Named
const namedObj = addName(obj, 'Jane');

23.4 控制流程

23.4.1 條件類型

條件類型 具有以下語法

«Type2» extends «Type1» ? «ThenType» : «ElseType»

如果 Type2 可指派給 Type1,則此類型表達式的結果為 ThenType。否則,為 ElseType

23.4.1.1 範例:僅封裝具有屬性 .length 的類型

在以下範例中,Wrap<> 僅將類型封裝在單元素組中,如果它們具有屬性 .length,其值為數字

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: [string]
type A = Wrap<string>;

// %inferred-type: RegExp
type B = Wrap<RegExp>;
23.4.1.2 範例:檢查可指派性

我們可以使用條件類型來實作可指派性檢查

type IsAssignableTo<A, B> = A extends B ? true : false;

// Type `123` is assignable to type `number`
// %inferred-type: true
type Result1 = IsAssignableTo<123, number>;

// Type `number` is not assignable to type `123`
// %inferred-type: false
type Result2 = IsAssignableTo<number, 123>;

如需有關類型關係可指派性的詳細資訊,請參閱 [未包含內容]

23.4.1.3 條件類型具有分配性

條件類型具有 分配性:將條件類型 C 套用至聯集類型 U 與將 C 套用至 U 的每個組成部分的聯集相同。以下是一個範例

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: boolean | [string] | [number[]]
type C1 = Wrap<boolean | string | number[]>;

// Equivalent:
type C2 = Wrap<boolean> | Wrap<string> | Wrap<number[]>;

換句話說,分配性使我們能夠「迴圈」聯集類型的組成部分。

以下是分配性的另一個範例

type AlwaysWrap<T> = T extends any ? [T] : [T];

// %inferred-type: ["a"] | ["d"] | [{ a: 1; } & { b: 2; }]
type Result = AlwaysWrap<'a' | ({ a: 1 } & { b: 2 }) | 'd'>;
23.4.1.4 使用具有分配性的條件類型,我們使用類型 never 來忽略項目

將類型 never 解釋為一個集合時,它是空的。因此,如果它出現在聯集類型中,它將被忽略

// %inferred-type: "a" | "b"
type Result = 'a' | 'b' | never;

這表示我們可以使用 never 來忽略聯集類型的組成部分

type DropNumbers<T> = T extends number ? never : T;

// %inferred-type: "a" | "b"
type Result1 = DropNumbers<1 | 'a' | 2 | 'b'>;

如果我們交換 then 分支和 else 分支的類型表達式,就會發生這種情況

type KeepNumbers<T> = T extends number ? T : never;

// %inferred-type: 1 | 2
type Result2 = KeepNumbers<1 | 'a' | 2 | 'b'>;
23.4.1.5 內建公用程式類型:Exclude<T, U>

從聯集中排除類型是一種非常常見的操作,因此 TypeScript 提供了內建公用程式類型 Exclude<T, U>

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

// %inferred-type: "a" | "b"
type Result1 = Exclude<1 | 'a' | 2 | 'b', number>;

// %inferred-type: "a" | 2
type Result2 = Exclude<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.6 內建公用程式類型:Extract<T, U>

Exclude<T, U> 的反向類型為 Extract<T, U>

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

// %inferred-type: 1 | 2
type Result1 = Extract<1 | 'a' | 2 | 'b', number>;

// %inferred-type: 1 | "b"
type Result2 = Extract<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.7 串連條件類型

類似於 JavaScript 的三元運算子,我們也可以串連 TypeScript 的條件類型運算子

type LiteralTypeName<T> =
  T extends undefined ? "undefined" :
  T extends null ? "null" :
  T extends boolean ? "boolean" :
  T extends number ? "number" :
  T extends bigint ? "bigint" :
  T extends string ? "string" :
  never;

// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;

// %inferred-type: "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
23.4.1.8 infer 和條件類型

https://typescript.dev.org.tw/docs/handbook/advanced-types.html#type-inference-in-conditional-types

範例

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

範例

type Syncify<Interf> = {
    [K in keyof Interf]:
        Interf[K] extends (...args: any[]) => Promise<infer Result>
        ? (...args: Parameters<Interf[K]>) => Result
        : Interf[K];
};

// Example:

interface AsyncInterface {
    compute(arg: number): Promise<boolean>;
    createString(): Promise<String>;
}

type SyncInterface = Syncify<AsyncInterface>;
    // type SyncInterface = {
    //     compute: (arg: number) => boolean;
    //     createString: () => String;
    // }

23.4.2 對應類型

對應類型 會透過迴圈處理一組鍵來產生一個物件,例如

// %inferred-type: { a: number; b: number; c: number; }
type Result = {
  [K in 'a' | 'b' | 'c']: number
};

運算子 in 是對應類型的重要部分:它指定新物件文字類型的鍵來自何處。

23.4.2.1 內建公用程式類型:Pick<T, K>

下列內建公用程式類型讓我們能夠透過指定我們要保留現有物件類型的哪些屬性來建立新的物件

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

使用方式如下

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
23.4.2.2 內建公用程式類型:Omit<T, K>

下列內建公用程式類型讓我們能夠透過指定我們要省略現有物件類型的哪些屬性來建立新的物件類型

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

說明

Omit<> 的使用方式如下

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;

23.5 其他各種運算子

23.5.1 索引類型查詢運算子 keyof

我們已經遇到過類型運算子 keyof。它會列出物件類型的屬性鍵

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: 0 | 1 | "prop0" | "prop1"
type Result = keyof Obj;

keyof 套用至元組類型會產生可能有點出乎意料的結果

// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];

結果包含

空物件文字類型的屬性鍵為空集合 never

// %inferred-type: never
type Result = keyof {};

以下是 keyof 處理交集類型和聯集類型的方式

type A = { a: number, shared: string };
type B = { b: number, shared: string };

// %inferred-type: "a" | "b" | "shared"
type Result1 = keyof (A & B);

// %inferred-type: "shared"
type Result2 = keyof (A | B);

如果我們記得 A & B 具有類型 A 和類型 B 兩者的 屬性,這就說得通了。AB 只有屬性 .shared 共通,這解釋了 Result2

23.5.2 索引存取運算子 T[K]

索引存取運算子 T[K] 會傳回 T 中所有屬性的類型,其鍵可指定給類型 KT[K] 也稱為查詢類型

以下是此運算子的使用範例

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: "a" | "b"
type Result1 = Obj[0 | 1];

// %inferred-type: "c" | "d"
type Result2 = Obj['prop0' | 'prop1'];

// %inferred-type: "a" | "b" | "c" | "d"
type Result3 = Obj[keyof Obj];

括號中的類型必須可指定給所有屬性鍵的類型(由 keyof 計算)。這就是為什麼不允許 Obj[number]Obj[string]。不過,如果索引類型有索引簽章,我們可以使用 numberstring 作為索引類型(第 A 行)

type Obj = {
  [key: string]: RegExp, // (A)
};

// %inferred-type: string | number
type KeysOfObj = keyof Obj;

// %inferred-type: RegExp
type ValuesOfObj = Obj[string];

KeysOfObj 包含類型 number,因為數字鍵是 JavaScript(因此也是 TypeScript)中字串鍵的子集。

元組類型也支援索引存取

type Tuple = ['a', 'b', 'c', 'd'];

// %inferred-type:  "a" | "b"
type Elements = Tuple[0 | 1];

括號運算子也是分配式的

type MyType = { prop: 1 } | { prop: 2 } | { prop: 3 };

// %inferred-type: 1 | 2 | 3
type Result1 = MyType['prop'];

// Equivalent:
type Result2 =
  | { prop: 1 }['prop']
  | { prop: 2 }['prop']
  | { prop: 3 }['prop']
;

23.5.3 類型查詢運算子 typeof

類型運算子 typeof 會將(JavaScript)值轉換為其(TypeScript)類型。其運算元必須是識別碼或點分隔識別碼序列

const str = 'abc';

// %inferred-type: "abc"
type Result = typeof str;

第一個 'abc' 是值,而第二個 "abc" 是其類型,字串文字類型。

以下是使用 typeof 的另一個範例

const func = (x: number) => x + x;
// %inferred-type: (x: number) => number
type Result = typeof func;

§14.1.2「將符號新增至類型」 說明了 typeof 一個有趣的用例。