keyof
T[K]
typeof
在本章中,我們將探討如何在 TypeScript 中於編譯時使用類型運算。
請注意,本章的重點在於學習如何使用類型運算。因此,我們將大量使用文字類型,而範例與實際應用較無關。
考慮以下兩個層級的 TypeScript 程式碼
類型層級是程式層級的元層級。
層級 | 可用於 | 運算元 | 運算 |
---|---|---|---|
程式層級 | 執行時 | 值 | 函式 |
類型層級 | 編譯時 | 特定類型 | 泛型類型 |
我們可以進行類型運算,這代表什麼意思?以下程式碼是一個範例
type ObjectLiteralType = {
: 1,
first: 2,
second;
}
// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)
在 A 行中,我們執行以下步驟
ObjectLiteralType
,一個物件文字類型。keyof
。它會列出物件類型的屬性金鑰。keyof
的輸出命名為 Result
。在類型層級,我們可以使用下列「值」進行運算
type ObjectLiteralType = {
: string,
prop1: number,
prop2;
}
interface InterfaceType {: string;
prop1: number;
prop2
}
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';
泛型類型是元層級的函式,例如
type Wrap<T> = [T];
泛型類型 Wrap<>
具有參數 T
。其結果為 T
,包裝在一個元組類型中。以下是我們如何使用這個元函數
// %inferred-type: [string]
type Wrapped = Wrap<string>;
我們將參數 string
傳遞給 Wrap<>
,並將結果給予別名 Wrapped
。結果是一個具有單一元件的元組類型,也就是類型 string
。
|
)類型運算子 |
用於建立聯合類型
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'
;
TypeScript 將元值的集合表示為文字類型的聯合。我們已經看過一個範例
type Obj = {
: 1,
first: 2,
second;
}
// %inferred-type: "first" | "second"
type Result = keyof Obj;
我們很快就會看到用於迴圈處理此類集合的類型層級運算。
由於聯合類型中的每個成員都是至少一個元件類型的成員,因此我們只能安全地存取所有元件類型共有的屬性 (A 行)。若要存取任何其他屬性,我們需要類型防護 (B 行)
type ObjectTypeA = {
: bigint,
propA: string,
sharedProp
}type ObjectTypeB = {
: boolean,
propB: string,
sharedProp
}
type Union = ObjectTypeA | ObjectTypeB;
function func(arg: Union) {
// string
.sharedProp; // (A) OK
arg// @ts-expect-error: Property 'propB' does not exist on type 'Union'.
.propB; // error
arg
if ('propB' in arg) { // (B) type guard
// ObjectTypeB
;
arg
// boolean
.propB;
arg
} }
&
)類型運算子 &
用於建立交集類型
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
// %inferred-type: "b" | "c"
type Intersection = A & B;
如果我們將類型 A
和類型 B
視為集合,則 A & B
是這些集合的集合論交集。換句話說:結果的成員是兩個運算元的成員。
兩個物件類型的交集具有兩種類型的屬性
type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
type Both = {
: boolean,
prop1: number,
prop2;
}
// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;
(泛型類型 IsAssignableTo<>
會在稍後說明。)
如果我們將物件類型 Named
混入另一個類型 Obj
中,則我們需要一個交集類型 (A 行)
interface Named {: string;
name
}function addName<Obj extends object>(obj: Obj, name: string)
: Obj & Named // (A)
{= obj as (Obj & Named);
const namedObj .name = name;
namedObj;
return namedObj
}
= {
const obj : 'Doe',
last;
}
// %inferred-type: { last: string; } & Named
= addName(obj, 'Jane'); const namedObj
條件類型 具有以下語法
? «ThenType» : «ElseType» «Type2» extends «Type1»
如果 Type2
可指派給 Type1
,則此類型表達式的結果為 ThenType
。否則,為 ElseType
。
.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>;
我們可以使用條件類型來實作可指派性檢查
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>;
如需有關類型關係可指派性的詳細資訊,請參閱 [未包含內容]。
條件類型具有 分配性:將條件類型 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'>;
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'>;
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'>;
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'>;
類似於 JavaScript 的三元運算子,我們也可以串連 TypeScript 的條件類型運算子
type LiteralTypeName<T> =
undefined ? "undefined" :
T extends null ? "null" :
T extends boolean ? "boolean" :
T extends number ? "number" :
T extends bigint ? "bigint" :
T extends string ? "string" :
T extends never;
// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;
// %inferred-type: "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
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> = {
keyof Interf]:
[K in extends (...args: any[]) => Promise<infer Result>
Interf[K] ? (...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;
// }
對應類型 會透過迴圈處理一組鍵來產生一個物件,例如
// %inferred-type: { a: number; b: number; c: number; }
type Result = {
'a' | 'b' | 'c']: number
[K in ; }
運算子 in
是對應類型的重要部分:它指定新物件文字類型的鍵來自何處。
Pick<T, K>
下列內建公用程式類型讓我們能夠透過指定我們要保留現有物件類型的哪些屬性來建立新的物件
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
: T[P];
[P in K]; }
使用方式如下
type ObjectLiteralType = {
: 1,
eeny: 2,
meeny: 3,
miny: 4,
moe;
}
// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
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>>;
說明
K extends keyof any
表示 K
必須是所有屬性鍵類型的子類型
// %inferred-type: string | number | symbol
type Result = keyof any;
Exclude<keyof T, K>>
表示:取得 T
的鍵,並移除 K
中提到的所有「值」。
Omit<>
的使用方式如下
type ObjectLiteralType = {
: 1,
eeny: 2,
meeny: 3,
miny: 4,
moe;
}
// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;
keyof
我們已經遇到過類型運算子 keyof
。它會列出物件類型的屬性鍵
type Obj = {
0: 'a',
1: 'b',
: 'c',
prop0: 'd',
prop1;
}
// %inferred-type: 0 | 1 | "prop0" | "prop1"
type Result = keyof Obj;
將 keyof
套用至元組類型會產生可能有點出乎意料的結果
// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];
結果包含
"0" | "1" | "2"
number
.length
的名稱Array
方法的名稱:"pop" | "push" | ···
空物件文字類型的屬性鍵為空集合 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
兩者的 屬性,這就說得通了。A
和 B
只有屬性 .shared
共通,這解釋了 Result2
。
T[K]
索引存取運算子 T[K]
會傳回 T
中所有屬性的類型,其鍵可指定給類型 K
。T[K]
也稱為查詢類型。
以下是此運算子的使用範例
type Obj = {
0: 'a',
1: 'b',
: 'c',
prop0: 'd',
prop1;
}
// %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]
。不過,如果索引類型有索引簽章,我們可以使用 number
和 string
作為索引類型(第 A 行)
type Obj = {
: string]: RegExp, // (A)
[key;
}
// %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']
;
typeof
類型運算子 typeof
會將(JavaScript)值轉換為其(TypeScript)類型。其運算元必須是識別碼或點分隔識別碼序列
= 'abc';
const str
// %inferred-type: "abc"
type Result = typeof str;
第一個 'abc'
是值,而第二個 "abc"
是其類型,字串文字類型。
以下是使用 typeof
的另一個範例
= (x: number) => x + x;
const func // %inferred-type: (x: number) => number
type Result = typeof func;
§14.1.2「將符號新增至類型」 說明了 typeof
一個有趣的用例。