简介
同步一些 TypeScript 小技巧,和较隐晦的知识点,需要较为扎实的 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"
对象属性约束
对象属性默认约束为 string | number | symbol
联合类型。
为了方便使用,TS 提供了 PropertyKey
类型别名,表示 string | number | symbol
联合类型,使用 keyof any
也同样返回该类型。
type T1 = PropertyKey
// type T1 = string | number | symbol
type T2 = keyof any
// type T2 = string | number | symbol
空对象类型
JavaScript 中一切数据皆为对象,所以在 TypeScript 中,空对象类型可以赋值除了 null
和 undefined
之外的任何类型。
type A = {}
const a: A = 123 // ok
const b: A = 'str' // ok
const e: A = {} // ok
const c: A = null // ok
Error: // 类型'null'不能赋值给类型'A'
const d: A = undefined
Error: // 类型'undefined'不能赋值给类型'A'
v4.8 改进交叉类型简化、联合类型兼容性,以及类型缩窄
TypeScript 4.8 版本为 --strictNullChecks
带来了一系列修正和改进。
unknown
可表示为 {} | undefined | null
function f(x: unknown, y: {} | null | undefined) {
x = y; // 正确
y = x; // 以前报错, 4.8 及之后正确
}
与空对象类型 {}
交叉会消除 null
和 undefined
,所以内置工具类型 NonNullable
被修改为:
// v4.8 之前
type NonNullable<T> = T extends null | undefined ? never : T;
// v4.8 及之后
type NonNullable<T> = T & {};
现在可把 NonNullable<NonNullable<T>>
简化为 NonNullable<T>
function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
x = y; // 一直没问题
y = x; // 以前会报错,现在没问题
}
还带来了更合理的在控制流分析中类型缩窄:
function narrowUnknownishUnion(x: {} | null | undefined) {
if (x) {
x; // {}
}
else {
x; // {} | null | undefined
}
}
function narrowUnknown(x: unknown) {
if (x) {
x; // 以前是 'unknown',现在是 '{}'
}
else {
x; // unknown
}
}
泛型也会进行类似的缩窄。 当检查一个值不为 null
或 undefined
时, TypeScript 会将其与 {}
进行交叉 - 等同于使用 NonNullable
。
function throwIfNullable<T>(value: T): NonNullable<T> {
if (value === undefined || value === null) {
throw Error("Nullable value!");
}
// 以前会报错,因为 'T' 不能赋值给 'NonNullable<T>'。
// 现在不报错,因为会缩窄为 'T & {}' ,等同于 'NonNullable<T>'。
return value;
}
交叉类型
交叉类型中,如果交叉的成员如果是兼容关系,那会只会返回"窄"类型成员。所以,和 unknown
交叉会消除 unknown
返回其它交叉成员。如果与 any
交叉,那么只返回 any
。如果不兼容,则返回 never
。
联合类型和联合类型交叉,只返回有交集的联合成员。
type A1 = ('a' | 'b' | 'c') & ('b' | 'c')
// type A1 = "b" | "c"
type A2 = ('a' | 'b' | 'c') & 'b'
// type A2 = "b"
type A3 = ('a' | 'b' | 'c') & undefined
// type A3 = never
type A4 = 123 & null
// type A4 = never
type A5 = ('a' | 'b' | 'c') & unknown
// type A5 = "a" | "b" | "c"
type A6 = string & 'str'
// type A6 = "str"
type A7 = ('a' | 'b' | 'c') & any
// type A7 = any
联合类型
联合类型中,如果有的联合类型成员是兼容关系,那么只会返回"宽"类型成员。所以联合类型如果包含 unknown
,那么会消除其它联合成员,只剩下 unknown
,any
也一样,并且 any
优先级比unknown
高。
type A1 = string | number | 'str'
// type A1 = string | number
type A2 = string | number | unknown
// type A2 = unknown
type A3 = string | number | any
// type A3 = any
type A4 = string | number | any | unknown
// type A5 = any
type A5 = string | number | undefined
// type A5 = string | number | undefined
type A6 = string | number | null
// type A6 = string | number | null
感谢观看,如有错误,望指正,欢迎留言讨论。
后边有新知识点会添加更新本文章,待续......