你不知道的 TypeScript
Equality
判断两个类型是否全等
type IsEqual<T, U> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2)
? true
: false;
type T1 = IsEqual<22, 22> // true
type T2 = IsEqual<22, number> // false
原理:
它依赖于当 G
未知时,延迟的条件类型。延迟条件类型的可赋值,依赖于内部的 isTypeIdenticalTo
检查,该检查仅对以下两个条件类型为真:
- 两种条件类型具有相同的约束
- 两个条件的真和假分支是同一类型
也就是说,满足以上两个条件的时候,G
会自行推断,走 true
分支。
isTypeIdenticalTo
方法定义在 /node_modules/typescript/lib/typescript.js
keyof 类型操作符
keyof
应用于联合类型
keyof
类型操作符应用于联合类型,那么获取的是联合类型成员共同拥有的属性,如果没有共同拥有的属性,那么返回 never
。
type X2 = { a: string } | { b: number }
type T2 = keyof X2
// type T2 = never
// ======cut=======
type X2 = { a: string } | { a: number, b: number }
type T2 = keyof X2
// type T2 = "a"
keyof
应用于原始类型
keyof
操作符只能应用于对象类型,如果应用于 number
类型(keyof number
),那么 TypeScript 会自动将 number
转化为其对应的对象类型,也就是 keyof Number
,从而获得 Number
内置对象类型的所有属性和方法。
这些属性是由 JavaScript 引擎在运行时提供的,代表了 number
类型所支持的方法和属性。
type Num = keyof number
// type Num = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type T1<T> = keyof T
type Num2 = T1<number>
// type Num2 = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type Num3 = { [k in keyof number]: 1 };
// type Num3 = {
// toString: 1;
// toFixed: 1;
// toExponential: 1;
// toPrecision: 1;
// valueOf: 1;
// toLocaleString: 1;
// }
type T2<T> = { [k in keyof T]: 1 };
type Num4 = T2<number>
// type Num4 = number
就像 JavaScript 的包装对象,基础数据类型获取属性的时候,JavaScript 引擎会把基础数据类型转为临时的包装对象。这里不过多讨论,可参考:包装对象。
例子中 Num4
的结果看起来很矛盾,而这都是类型系统结合上下文推断泛型参数 T
的结果。具体如何推断,还不太清楚。
Record<K,V>
可利用内置工具类型 Record
快速创建一个对象索引签名类型,可用来判断对象类型(函数,数组,Set 等,都是对象)。
type Obj = Record<keyof any,any>
// type Obj = {
// [x: string]: any;
// [x: number]: any;
// [x: symbol]: any;
// }
数组
number 属性
访问数组类型的 number
属性,会获得数组所有的元素类型,并组成联合类型。
type T1 = [123, '456', true][number]
// type T1 = true | 123 | "456"
type T2 = (number|string)[][number]
// type T2 = string | number
type T3 = ReadonlyArray<number|string>[number]
// type T2 = string | number
length 属性
访问数组类型的 length
属性,会获得元组类型的长度,如果是数组的话,会得到 number
,因为数组没有固定长度。
type T1 = [123, '456', true]['length']
// type T1 = 3
type T2 = (number|string)[]['length']
// type T2 = number
扩展运算符
JavaScript 中,只能收集数组剩余的元素,就是说只能用在最后。
而 TypeScript 可在任何地方收集数组元素。
type T1<T> = T extends [...infer Sets, infer Last] ? [Sets, Last] : T
type Arr = T1<[123, '456', 789, true]>
// type Arr = [[123, "456"], true]
type T2<T> = T extends [infer First, ...infer Sets, infer Last] ? [First, Sets, Last] : T
type Arr2 = T2<[123, '456', 789, true]>
// type Arr2 = [123, ["456", 789], true]
type T3<T> = T extends [infer First, ...infer Sets] ? [First, Sets,] : T
type Arr3 = T3<[123, '456', 789, true]>
// type Arr3 = [123, ["456", 789, true]]
扩展用于泛型,如下函数,获取参数除了第一个元素的剩余元素
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
const r1 = tail(myTuple);
// const r1: [2, 3, 4]
const r2 = tail([...myTuple, ...myArray] as const);
// const r2: [2, 3, 4, ...string[]]
更复杂的例子
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}
// ---cut---
const foo = (x: string, y: number, z: boolean) => {};
const f1 = partialCall(foo, 100);
Error:// Argument of type 'number' is not assignable to parameter of type 'string'.
const f2 = partialCall(foo, "hello", 100, true, "oops");
Error:// Expected 4 arguments, but got 5.
// This works!
const f3 = partialCall(foo, "hello");
// const f3: (y: number, z: boolean) => void
// What can we do with f3 now?
// Works!
f3(123, true);
f3();
Error:// Expected 2 arguments, but got 0.
f3(123, "hello");
Error:// Argument of type 'string' is not assignable to parameter of type 'boolean'.
Interface
Interface 不支持映射类型
目前 TypeScript 还不支持在接口中使用映射类型。这是因为在接口中使用映射类型会导致一些类型不稳定的问题。
因为接口会 声明合并,而映射类型的结果是根据原类型生成的,如果原类型被修改或者扩展,那么映射类型的结果也会发生变化,这会导致代码的不稳定性。
因此,如果你需要使用映射类型,可以将它定义为一个类型别名(type alias),而不是接口。类型别名与接口类似,但是它是一个给类型起别名的方式,不是定义新的类型,而且也不会发生声明合并。
// Error
interface Stringify<T> {
[P in keyof T]: string;
}
// OK
type Stringify2<T> = { [P in keyof T]: string };
类型断言
TypeScript 只允许类型断言为更具体或更宽松的类型。这条规则可以防止出现 "不可能的" 强制类型转换,例如:
const x = "hello" as number;
// 转换 "string" 类型为 "number" 类型可能是错误的,因为两种类型不能充分重叠。如果是故意的,请先将表达式转换为 "unknown" 再转 "number"
可先转 any
或 unknown
便可再转其它任意类型。
const a = (expr as any) as T;
分发 (distributing)
TypeScript 在分发条件时将 never
视为空联合
这意味着 "a" | never
在分配时被缩短为 "a"
。这也意味着 'a' | (never | 'b') | (never | never)
在分布时就变成了 'a' | 'b'
,因为 never
部分相当于一个空的并集。
所以会出现如下问题:
type T1<T> = T extends never ? true : false;
type T2 = T1<never>
// type T2 = never
T2
类型传入泛型参数 never
,永远不会走 true
分支或 false
分支。因为泛型参数 T
为 never
,在分发条件时被视为一个空联合。
解决方案为:
type T1<T> = [T] extends [never] ? true : false;
// 或
type T1<T> = T[] extends never[] ? true : false;
type T2 = T1<never>
// type T2 = true
把泛型参数 T
当作数组元素,再和有 never
元素的数组进行对比。这样就不再是把 never
当成联合类型,进行分发条件,避免了之前的问题。
模板字面量
模板字符串除了用来拼接组合以外,还可以用来拆分替换。
type T1<S extends string, From extends string, To extends string> =
S extends `${infer F}${From}${infer R}`
? `${F}${To}${R}`
: never
type S1 = T1<"I don't like to code", "don't", "really">
// type S1 = "I really like to code"
感谢观看,如有错误,望指正,欢迎留言讨论。
后边有新知识点会添加更新本文章,待续......
转载自:https://juejin.cn/post/7236295340629835834