keyofT[K]typeof在本章中,我们将探讨如何在 TypeScript 中在编译时使用类型进行计算。
请注意,本章的重点是学习如何使用类型进行计算。因此,我们将大量使用字面量类型,并且示例的实际相关性较低。
考虑以下两个级别的 TypeScript 代码
类型级别是程序级别的元级别。
| 级别 | 可用时间 | 操作数 | 操作 |
|---|---|---|---|
| 程序级别 | 运行时 | 值 | 函数 |
| 类型级别 | 编译时 | 特定类型 | 泛型 |
使用类型进行计算意味着什么?以下代码是一个示例
type ObjectLiteralType = {
first: 1,
second: 2,
};
// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)在 A 行中,我们正在采取以下步骤
ObjectLiteralType,一个对象字面量类型。keyof 应用于输入。它列出了对象类型的属性键。keyof 的输出命名为 Result。在类型级别,我们可以使用以下“值”进行计算
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';泛型是元级别的函数 - 例如
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 = {
first: 1,
second: 2,
};
// %inferred-type: "first" | "second"
type Result = keyof Obj;我们很快就会看到用于循环访问此类集合的类型级别操作。
由于联合类型的每个成员都是至少一个组件类型的成员,我们只能安全地访问所有组件类型共享的属性(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;
}
}&)类型运算符 & 用于创建交叉类型
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 = {
prop1: boolean,
prop2: number,
};
// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;(泛型 IsAssignableTo<> 将在后面解释。)
如果我们将对象类型 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');条件类型具有以下语法
«Type2» extends «Type1» ? «ThenType» : «ElseType»如果 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> =
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'>;infer 和条件类型https://typescript.net.cn/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;
// }映射类型通过循环访问键集合来生成对象 - 例如
// %inferred-type: { a: number; b: number; c: number; }
type Result = {
[K in 'a' | 'b' | 'c']: number
};运算符 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> = {
[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'>;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 = {
eeny: 1,
meeny: 2,
miny: 3,
moe: 4,
};
// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;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'];结果包括
"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',
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] 的原因。但是,如果索引类型具有索引签名,则我们可以使用 number 和 string 作为索引类型(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']
;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 的一个有趣用例。