likes
comments
collection
share

你不知道的 TypeScript

作者站长头像
站长
· 阅读数 16

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

你不知道的 TypeScript

参考资料:github.com/microsoft/T…

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

而 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"

可先转 anyunknown 便可再转其它任意类型。

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 分支。因为泛型参数 Tnever ,在分发条件时被视为一个空联合。

解决方案为:

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"

感谢观看,如有错误,望指正,欢迎留言讨论。

后边有新知识点会添加更新本文章,待续......