你不知道的 TypeScript

372 阅读8分钟

简介

同步一些 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

image.png

参考资料: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 中,只能收集数组剩余的元素,就是说只能用在最后。

image.png

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

对象属性约束

对象属性默认约束为 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 中,空对象类型可以赋值除了 nullundefined 之外的任何类型。

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 及之后正确
}

与空对象类型 {} 交叉会消除 nullundefined,所以内置工具类型 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,那么会消除其它联合成员,只剩下 unknownany 也一样,并且 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

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

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