05-套路一:模式匹配做提取
课程
1
如何阅读本小册
已学完
学习时长: 2分6秒
2
为什么说 TypeScript 的火爆是必然?
已学完
学习时长: 4分28秒
3
TypeScript 类型编程为什么被叫做类型体操?
已学完
学习时长: 9分16秒
4
TypeScript 类型系统支持哪些类型和类型运算?
已学完
学习时长: 19分27秒
5
套路一:模式匹配做提取
已学完
学习时长: 41分32秒
6
套路二:重新构造做变换
已学完
学习时长: 39分51秒
7
套路三:递归复用做循环
学习时长: 42分22秒
8
套路四:数组长度做计数
学习时长: 27分58秒
9
套路五:联合分散可简化
学习时长: 19分36秒
10
套路六:特殊特性要记清
学习时长: 36分41秒
11
类型体操顺口溜
学习时长: 27分26秒
12
TypeScript 内置的高级类型有哪些?
学习时长: 31分59秒
13
真实案例说明类型编程的意义
学习时长: 34分36秒
14
类型编程综合实战一
学习时长: 25分55秒
15
类型编程综合实战二
学习时长: 29分9秒
16
新语法 infer extends 是如何简化类型编程的
学习时长: 11分14秒
17
原理篇:逆变、协变、双向协变、不变
学习时长: 11分31秒
18
原理篇:编译 ts 代码用 tsc 还是 babel?
学习时长: 19分49秒
19
原理篇:实现简易 TypeScript 类型检查
学习时长: 47分51秒
20
原理篇:如何阅读 TypeScript 源码
学习时长: 19分27秒
21
原理篇:一些特殊情况的说明
已学完
学习时长: 12分39秒
22
小册总结
学习时长: 2分32秒
23
加餐:3 种类型来源和 3 种模块语法
学习时长: 17分6秒
24
加餐:用 Project Reference 优化 tsc 编译性能
已学完
学习时长: 4分47秒
juejin_logo copyCreated with Sketch.

TypeScript 类型编程的代码看起来比较复杂,但其实这些逻辑用 JS 大家都会写,之所以到了类型体操就不会了,那是因为还不熟悉一些套路。

所以,这节开始我们就来学习一些类型体操的套路,熟悉这些套路之后,各种类型体操逻辑就能够很顺畅的写出来。

首先,我们来学习类型体操的第一个套路:模式匹配做提取。

模式匹配

我们知道,字符串可以和正则做模式匹配,找到匹配的部分,提取子组,之后可以用 1,1,2 等引用匹配的子组。

Typescript 的类型也同样可以做模式匹配。

比如这样一个 Promise 类型:

type p = Promise<'guang'>;

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

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

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

试一下

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

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

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

数组类型

First

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

type arr = [1,2,3]

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

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

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

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

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

当类型参数 Arr 为 [1,2,3] 时:

当类型参数 Arr 为 [] 时:

试一下

Last

可以提取第一个元素,当然也可以提取最后一个元素,修改下模式类型就行:

type GetLast<Arr extends unknown[]> = 
    Arr extends [...unknown[], infer Last] ? Last : never;

当类型参数 Arr 为 [1,2,3]时:

试一下

PopArr

我们分别取了首尾元素,当然也可以取剩余的数组,比如取去掉了最后一个元素的数组:

type PopArr<Arr extends unknown[]> = 
    Arr extends [] ? [] 
        : Arr extends [...infer Rest, unknown] ? Rest : never;

如果是空数组,就直接返回,否则匹配剩余的元素,放到 infer 声明的局部变量 Rest 里,返回 Rest。

当类型参数 Arr 为 [1,2,3] 时:

当类型参数 Arr 为 [] 时:

试一下

ShiftArr

同理可得 ShiftArr 的实现:

type ShiftArr<Arr extends unknown[]> = 
    Arr extends [] ? [] 
        : Arr extends [unknown, ...infer Rest] ? Rest : never;

当类型参数 Arr 为 [1,2,3]时:

试一下

字符串类型

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

StartsWith

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

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

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

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

当匹配时:

不匹配时:

试一下

Replace

字符串可以匹配一个模式类型,提取想要的部分,自然也可以用这些再构成一个新的类型。

比如实现字符串替换:

type ReplaceStr<
    Str extends string,
    From extends string,
    To extends string
> = Str extends `${infer Prefix}${From}${infer Suffix}` 
        ? `${Prefix}${To}${Suffix}` : Str;

声明要替换的字符串 Str、待替换的字符串 From、替换成的字符串 3 个类型参数,通过 extends 约束为都是 string 类型。

用 Str 去匹配模式串,模式串由 From 和之前之后的字符串构成,把之前之后的字符串放到通过 infer 声明的局部变量 Prefix、Suffix 里。

用 Prefix、Suffix 加上替换到的字符串 To 构造成新的字符串类型返回。

当匹配时:

不匹配时:

试一下

Trim

能够匹配和替换字符串,那也就能实现去掉空白字符的 Trim:

不过因为我们不知道有多少个空白字符,所以只能一个个匹配和去掉,需要递归。

先实现 TrimRight:

type TrimStrRight<Str extends string> = 
    Str extends `${infer Rest}${' ' | '\n' | '\t'}` 
        ? TrimStrRight<Rest> : Str;

类型参数 Str 是要 Trim 的字符串。

如果 Str 匹配字符串 + 空白字符 (空格、换行、制表符),那就把字符串放到 infer 声明的局部变量 Rest 里。

把 Rest 作为类型参数递归 TrimRight,直到不匹配,这时的类型参数 Str 就是处理结果。

同理可得 TrimLeft:

type TrimStrLeft<Str extends string> = 
    Str extends `${' ' | '\n' | '\t'}${infer Rest}` 
        ? TrimStrLeft<Rest> : Str;

TrimRight 和 TrimLeft 结合就是 Trim:

type TrimStr<Str extends string> =TrimStrRight<TrimStrLeft<Str>>;

试一下

函数

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

GetParameters

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

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

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

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

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

试一试

GetReturnType

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

type GetReturnType<Func extends Function> = 
    Func extends (...args: any[]) => infer ReturnType 
        ? ReturnType : never;

Func 和模式类型做匹配,提取返回值到通过 infer 声明的局部变量 ReturnType 里返回。

参数类型可以是任意类型,也就是 any[](注意,这里不能用 unknown,因为参数类型是要赋值给别的类型的,而 unknown 只能用来接收类型,所以用 any)。

试一试

GetThisParameterType

方法里可以调用 this,比如这样:

class Dong {
    name: string;

    constructor() {
        this.name = "dong";
    }

    hello() {
        return 'hello, I\'m ' + this.name;
    }
}

const dong = new Dong();
dong.hello();

对象.方法名的方式调用的时候,this 就指向那个对象。

但是方法也可以用 call 或者 apply 调用:

call 调用的时候,this 就变了,但这里却没有被检查出来 this 指向的错误。

如何让编译器能够检查出 this 指向的错误呢?

可以在方法声明时指定 this 的类型:

class Dong {
    name: string;

    constructor() {
        this.name = "dong";
    }

    hello(this: Dong) {
        return 'hello, I\'m ' + this.name;
    }
}

这样,当 call/apply 调用的时候,就能检查出 this 指向的对象是否是对的:

如果没有报错,说明没开启 strictBindCallApply 的编译选项,这个是控制是否按照原函数的类型来检查 bind、call、apply

这里的 this 类型同样也可以通过模式匹配提取出来:

type GetThisParameterType<T> 
    = T extends (this: infer ThisType, ...args: any[]) => any 
        ? ThisType 
        : unknown;

类型参数 T 是待处理的类型。

用 T 匹配一个模式类型,提取 this 的类型到 infer 声明的局部变量 ThisType 中,其余的参数是任意类型,也就是 any,返回值也是任意类型。

返回提取到的 ThisType。

这样就能提取出 this 的类型:

试一下

构造器

构造器和函数的区别是,构造器是用于创建对象的,所以可以被 new。

同样,我们也可以通过模式匹配提取构造器的参数和返回值的类型:

GetInstanceType

构造器类型可以用 interface 声明,使用 new(): xx 的语法。

比如:

interface Person {
    name: string;
}

interface PersonConstructor {
    new(name: string): Person;
}

这里的 PersonConstructor 返回的是 Person 类型的实例对象,这个也可以通过模式匹配取出来。

type GetInstanceType<
    ConstructorType extends new (...args: any) => any
> = ConstructorType extends new (...args: any) => infer InstanceType 
        ? InstanceType 
        : any;

类型参数 ConstructorType 是待处理的类型,通过 extends 约束为构造器类型。

用 ConstructorType 匹配一个模式类型,提取返回的实例类型到 infer 声明的局部变量 InstanceType 里,返回 InstanceType。

这样就能取出构造器对应的实例类型:

试一下

GetConstructorParameters

GetInstanceType 是提取构造器返回值类型,那同样也可以提取构造器的参数类型:

type GetConstructorParameters<
    ConstructorType extends new (...args: any) => any
> = ConstructorType extends new (...args: infer ParametersType) => any
    ? ParametersType
    : never;

类型参数 ConstructorType 为待处理的类型,通过 extends 约束为构造器类型。

用 ConstructorType 匹配一个模式类型,提取参数的部分到 infer 声明的局部变量 ParametersType 里,返回 ParametersType。

这样就能提取出构造器对应的参数类型:

试一下

索引类型

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

我们简化一下那个高级类型,提取 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。

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

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

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

当 ref 为 undefined 时:

试一下

总结

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

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

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

本文案例的合并

留言
Ctrl + Enter
全部评论(104)
踏雪1024的头像
删除
type PopArr<Arr extends unknown[]> = Arr extends [...infer Rest, unknown]
? Rest
: []

这样就不用 Arr extends [] ? []
点赞
回复
wyswill的头像
删除
engineer @ Stark Industries
```js
class Dong { name: string; constructor() { this.name = 'dong'; } hello(this: Dong, asdf: string) { return 'hello, I\'m ' + this.name; } } // cal bind 检测 在 Object.assign 方法中可以绕过 new Dong().hello.call(Object.assign({ 'asd': 'asdf' }), 'asdas');
```
call 和 bind检测 可以通过Object.assing 绕过
点赞
回复
感觉时刻的头像
删除
模式匹配提取,学到了
点赞
回复
在工作的L的头像
删除
前端 @ 碳衡科技
getReturnType中的`unknown 只能用来接收类型,所以用 any`,这个怎么理解呀
点赞
4
删除
(作者)
额,主要是 any 是既可以接收类型,也可以赋值给其他类型,而 unknown 只能接收类型,就是这个区别
点赞
回复
删除
好的,我再学习学习[嘿哈]
额,主要是 any 是既可以接收类型,也可以赋值给其他类型,而 unknown 只能接收类型,就是这个区别
点赞
回复
查看更多回复
hedgehog_boy的头像
删除
错别字还是没改
点赞
1
删除
(作者)
[捂脸],待会改
点赞
回复
占用号的头像
删除
在 ts3.0 里面如果没有对应的索引,Obj[Key] 返回的是 {} 而不是 never,所以这样坐下兼容处理。 应该是做下兼容处理
1
1
删除
兄弟你好细呀[害羞]
1
回复
啊Ben学前端的头像
删除
前端 @ 字节跳动
打卡
点赞
回复
saswhite的头像
删除
菜鸡前端
后面再有那个Last内容只能获取到never,获或者写的时候语法检查直接报错了的同学注意下。请检查一下你的vscode支持当前workspace的typeScript的版本。不是说你的ts版本高,vscode就直接支持到最新版本,注意看一下这里,点击那个4.73的位置的话可以调整版本。
1
1
删除
我就是因为之前vscode一直支持的是4.12版本的ts,所以一直报错语法不通过之类的。
1
回复
似水年华1475130658147的头像
删除
前端开发工程师 @ MICAVEN
数组匹配中type GetLast<Arr extends unknown[]> = Arr extends [...unknown[] , infer Last]
? Last : never;这个为什么可以直接取到最后一个?如果套用es6扩展运算符是不支持的。
点赞
5
删除
你可以理解为解构赋值。
点赞
回复
删除
是可以理解为解构,但跟es6的解构是有区别的。es6的解构扩展运算符只能放在最后,如const [arr1,...arr2] = [1,2,3],而[...arr1,arr2]=[1,2,3]是会报错的
你可以理解为解构赋值。
点赞
回复
查看更多回复
狒狒君14014的头像
删除
前端工程师
请教大佬~ 【数组类型-Last】案例:
type GetLast<Arr extends unknown[]> =
Arr extends [...unknown[], infer Last] ? Last : never;

我运行GetLast<[1,2,3]>之后结果是never,这是为何~
点赞
2
删除
能拿到啊
点赞
回复
删除
应该是跟 ts 的版本有关,我用的 4.7.4 是可以成功做提取的,切换成 3.3.3 就是never了
点赞
回复
草苺奶昔的头像
删除
学生 @ 摸鱼
this 这里讲的很好
点赞
回复
一个假前端的头像
删除
web前端工程师
绑定this这个还是有点问题,只有name属性的时候报错
person.run.call({ name: 'wang' }) // 这个会报错
但是加了个run后就不报错了,这个怎么说呢?
点赞
2
删除
简单的解释
1
回复
删除
属性相交
简单的解释
点赞
回复
「骚年」的头像
删除
请教下作者,为什么在ts内置类型中,在限制泛型类型时,如:ThisParameterType,使用 any[] 来定义 ...args 参数,而到了ConstructorParameters中,new (...args: any) => any,...args 又使用了any来定义,这两种定义类型的方式有明确的说明,或者哪种更好吗?在尝试使用中,发现他俩基本一致,不会报什么错误
点赞
1
删除
(作者)
额,都行的
2
回复
国立的头像
删除
感谢神光老师,讲得很清晰。
有一个点想请教一下,就是PopArr或者ShiftArr的时候,可不可以去掉Arr extends []的判断,然后把最后返回的never变成空数组[],这样似乎也可以处理空数组的情况。
点赞
1
删除
(作者)
可以的,写法不唯一
点赞
回复
Yayoiqz的头像
删除
菜鸟前端工程师 @ 厂妹
体操入门+++,牛得很
点赞
回复
蒹葭问白露的头像
删除
前端 @ yy
打卡 ,直到下次再刷到這裡
1
1
删除
+1
点赞
回复
重威的头像
删除
很奇怪,vscode 里面这么写没报错,ts的版本是4.6.2
1
3
删除
同问
点赞
回复
删除
同问
点赞
回复
查看更多回复
一号的头像
删除
web前端 @ 新博卓畅技术(北京)有限公司
type GetParameters<Func extends Function> =
Func extends (...args: infer Args) => unknown ? Args : never;
问下这个返回的类型为什么是数组?参数里面没有定义是数组类型啊?
点赞
8
删除
(作者)
ts 就这么实现的
点赞
回复
删除
什么意思啊😂求大佬指点
点赞
回复
查看更多回复
温水里的鸭子的头像
删除
type GetInstanceType<T> = T extends new (args:any)=>infer res ? res:any;感觉这样会更简洁一点
1
1
删除
没有类型收紧,感觉
点赞
回复
了不起的戴先生的头像
删除
`这样,当 call/apply 调用的时候,就能检查出 this 指向的对象是否是对的:
` 这里应该指的是 this 指向的 对象 和 实例 类型是一致的吧?
点赞
1
删除
在 vscode run 的时候,确实会抛出异常。应该是线上工具的缺陷
点赞
回复

查看全部 104 条回复