Typescript进阶之类型体操套路一

128 阅读8分钟

类型体操

TypeScript 的类型编程代码可能看起来有些复杂,但实际上,这些逻辑用 JavaScript 编写时大家都会。之所以在类型体操中显得困难,主要是因为大家还不熟悉一些常用的技巧和模式。

因此,从这一节开始,我们将一起学习一些类型体操的常用套路。通过掌握这些技巧,你将会发现编写各种复杂的类型逻辑变得轻松自如。这些内容也受到了神光老师文章的启发。

套路一:模式匹配做提取

模式匹配

比如这样一个 Promise 类型:

type p = Promise<'guang'>;

我们想提取 value 的类型,可以这样做:

type GetValueType<P> = P extends Promise<infer Value> ? Value : never;

解释

  1. 泛型参数 P

    • P 是一个泛型参数,表示传入的类型。
    • P 需要满足 Promise<infer Value> 这个条件。
  2. 条件类型 P extends Promise<infer Value>

    • extends 在这里用于类型检查,确保 P 是一个 Promise 类型。
    • infer Value 用于从 Promise 类型中推断出解析值的类型 Value
  3. 条件类型的结果

    • 如果 P 是一个 Promise 类型,并且可以从中推断出解析值的类型 Value,则返回 Value
    • 否则,返回 never 类型,表示没有匹配到任何值。

通过 extends 对传入的类型参数 P 做模式匹配,其中值的类型是需要提取的,通过 infer 声明一个局部变量 Value 来保存,如果匹配,就返回匹配到的 Value,否则就返回 never 代表没匹配到。

// 示例 2: Promise<number>
type NumberPromiseValue = GetValueType<Promise<number>>; // number

// 示例 3: Promise<{ name: string }>
type ObjectPromiseValue = GetValueType<Promise<{ name: string }>>; // { name: string }

// 示例 4: 不是 Promise 类型
type NonPromiseValue = GetValueType<string>; // never
type StringPromiseValue = GetValueType<Promise<string>>; // string
  • Promise<string> 是一个 Promise 类型,解析值的类型是 string
  • 因此,StringPromiseValue 的类型是 string

这就是 Typescript 类型的模式匹配:

Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。

这个模式匹配的套路有多有用呢?我们来看下在数组、字符串、函数、构造器等类型里的应用。

数组类型

First

数组类型想提取第一个元素的类型怎么做呢?

type arr = [1,2,3]
​

用它来匹配一个模式类型,提取第一个元素的类型到通过 infer 声明的局部变量里返回。

type GetFirst<Arr extends unknown[]> = 
    Arr extends [infer First, ...unknown[]] ? First : never;
​

解释

  1. 泛型参数 Arr extends unknown[]

    • Arr 是一个泛型参数,表示传入的类型必须是一个数组或元组类型。
    • extends unknown[] 确保 Arr 是一个数组或元组类型。
  2. 条件类型 Arr extends [infer First, ...unknown[]]

    • [infer First, ...unknown[]] 是一个元组类型的模式匹配。
    • infer First 用于从数组或元组的第一个元素中推断出其类型 First
    • ...unknown[] 表示数组或元组的剩余部分,可以是任意类型的数组。
  3. 条件类型的结果

    • 如果 Arr 是一个非空数组或元组,并且可以从中推断出第一个元素的类型 First,则返回 First
    • 否则,返回 never 类型,表示没有匹配到任何值。

类型参数 Arr 通过 extends 约束为只能是数组类型,数组元素是 unkown 也就是可以是任何值。

any 和 unknown 的区别: any 和 unknown 都代表任意类型,但是 unknown 只能接收任意类型的值,而 any 除了可以接收任意类型的值,也可以赋值给任意类型(除了 never)。类型体操中经常用 unknown 接受和匹配任何类型,而很少把任何类型赋值给某个类型变量。

对 Arr 做模式匹配,把我们要提取的第一个元素的类型放到通过 infer 声明的 First 局部变量里,后面的元素可以是任何类型,用 unknown 接收,然后把局部变量 First 返回。


// 示例 1: 数组 [1, 2, 3]
type FirstNumber = GetFirst<[1, 2, 3]>; // number

// 示例 2: 元组 ['a', 'b', 'c']
type FirstString = GetFirst<['a', 'b', 'c']>; // string

字符串类型

字符串类型也同样可以做模式匹配,匹配一个模式字符串,把需要提取的部分放到 infer 声明的局部变量里。

StartsWith

判断字符串是否以某个前缀开头,也是通过模式匹配:

type StartsWith<Str extends string, Prefix extends string> = 
    Str extends `${Prefix}${string}` ? true : false;
​

解释

  1. 泛型参数 Str extends stringPrefix extends string

    • Str 是一个泛型参数,表示传入的类型必须是一个字符串。
    • Prefix 也是一个泛型参数,表示传入的前缀必须是一个字符串。
  2. 条件类型 Str extends 𝑃𝑟𝑒𝑓𝑖𝑥Prefix{string}`

    • ${Prefix}${string} 是一个模板字符串类型的模式匹配。
    • ${Prefix} 表示字符串 Str 必须以 Prefix 开头。
    • ${string} 表示字符串 Str 的剩余部分可以是任意字符串。
  3. 条件类型的结果

    • 如果 Str 匹配到 ${Prefix}${string} 这个模式,则返回 true
    • 否则,返回 false
// 示例 1: 字符串以指定前缀开头
type IsStartsWithHello = StartsWith<'Hello World', 'Hello'>; // true

// 示例 2: 字符串不以指定前缀开头
type IsStartsWithHi = StartsWith<'Hello World', 'Hi'>; // false

需要声明字符串 Str、匹配的前缀 Prefix 两个类型参数,它们都是 string。

用 Str 去匹配一个模式类型,模式类型的前缀是 Prefix,后面是任意的 string,如果匹配返回 true,否则返回 false。

函数

函数同样也可以做类型匹配,比如提取参数、返回值的类型。

GetParameters

函数类型可以通过模式匹配来提取参数的类型:

type GetParameters<Func extends Function> = 
    Func extends (...args: infer Args) => unknown ? Args : never;
​


// 示例 2: 函数有一个参数
type OneParam = GetParameters<(a: number) => void>; // [number]

// 示例 3: 函数有两个参数
type TwoParams = GetParameters<(a: number, b: string) => void>; // [number, string]
解释
  1. 泛型参数 Func extends Function

    • Func 是一个泛型参数,表示传入的类型必须是一个函数类型。
    • extends Function 确保 Func 是一个函数类型。
  2. 条件类型 Func extends (...args: infer Args) => unknown

    • (...args: infer Args) => unknown 是一个函数类型的模式匹配。
    • ...args: infer Args 表示函数可以接受任意数量和类型的参数,并且尝试从这些参数中推断出参数列表的类型 Args
    • => unknown 表示函数的返回值类型可以是任何类型,因为我们只关心参数列表的类型。
  3. 条件类型的结果

    • 如果 Func 是一个函数类型,并且可以从中推断出参数列表的类型 Args,则返回 Args
    • 否则,返回 never 类型,表示没有匹配到任何值。

类型参数 Func 是要匹配的函数类型,通过 extends 约束为 Function。

Func 和模式类型做匹配,参数类型放到用 infer 声明的局部变量 Args 里,返回值可以是任何类型,用 unknown。

返回提取到的参数类型 Args。

GetReturnType

能提取参数类型,同样也可以提取返回值类型:

type GetReturnType<Func extends Function> = 
    Func extends (...args: any[]) => infer ReturnType 
        ? ReturnType : never;
// 示例 1: 函数返回 number
type ReturnNumber = GetReturnType<() => number>; // number

// 示例 2: 函数返回 string
type ReturnString = GetReturnType<(name: string) => string>; // string

解释

  1. 泛型参数 Func extends Function

    • Func 是一个泛型参数,表示传入的类型必须是一个函数类型。
    • extends Function 确保 Func 是一个函数类型。
  2. 条件类型 Func extends (...args: any[]) => infer ReturnType

    • (...args: any[]) => infer ReturnType 是一个函数类型的模式匹配。
    • ...args: any[] 表示函数可以接受任意数量和类型的参数。
    • infer ReturnType 用于从函数类型中推断出返回值的类型 ReturnType
  3. 条件类型的结果

    • 如果 Func 是一个函数类型,并且可以从中推断出返回值的类型 ReturnType,则返回 ReturnType
    • 否则,返回 never 类型,表示没有匹配到任何值。

索引类型

索引类型也同样可以用模式匹配提取某个索引的值的类型,这个用的也挺多的,比如 React 的 index.d.ts 里的 PropsWithRef 的高级类型,就是通过模式匹配提取了 ref 的值的类型:

image.png

我们简化一下那个高级类型,提取 Props 里 ref 的类型:

GetRefProps

我们同样通过模式匹配的方式提取 ref 的值的类型:

type GetRefProps<Props> = 
    'ref' extends keyof Props
        ? Props extends { ref?: infer Value | undefined}
            ? Value
            : never
        : never;
​

类型参数 Props 为待处理的类型。

通过 keyof Props 取出 Props 的所有索引构成的联合类型,判断下 ref 是否在其中,也就是 'ref' extends keyof Props。

为什么要做这个判断,上面注释里写了:

image.png

在 ts3.0 里面如果没有对应的索引,Obj[Key] 返回的是 {} 而不是 never,所以这样做下兼容处理。

如果有 ref 这个索引的话,就通过 infer 提取 Value 的类型返回,否则返回 never。

image.png

总结

就像字符串可以匹配一个模式串提取子组一样,TypeScript 类型也可以匹配一个模式类型提取某个部分的类型。

TypeScript 类型的模式匹配是通过类型 extends 一个模式类型,把需要提取的部分放到通过 infer 声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理。

模式匹配的套路在数组、字符串、函数、构造器、索引类型、Promise 等类型中都有大量的应用,掌握好这个套路能提升很大一截类型体操水平。