vlambda博客
学习文章列表

几个一看就会的 TypeScript 小技巧

TypeScript 是一门语言,有很多语法,和那些只需要熟悉下 API 的库的层次不太一样,它更灵活,当然也会有很多小技巧。

这篇文章就来分享一些很多人不知道的小技巧吧,都是学完就能用起来的那种。

keyof any

TypeScript 有一个内置类型叫做 Record,它的作用是根据传入的索引和值的类型构造新的索引类型。

它的实现就是通过映射类型的语法构造一个索引类型:

type Record<K, T> = { [P in K]: T };

那么问题来了,这个 K 怎么约束呢?

有同学说 K 不是索引么?那应该是 string,也就是 K extends string。

但是 JS 的属性可以是 string、number、symbol 这三种类型的。

那我知道了,要 K extends string | number | symbol。

不不不,TypeScript 有个编译选项叫做 keyofStringsOnly,开启了那么就就只会用 string 作为索引,否则才是 string | number | symbol:

几个一看就会的 TypeScript 小技巧

这还与编译选项有关,那这里改怎么约束呢?

看下 TS 源码里是怎么定义 Record 的:

type Record<K extends keyof any, T> = { [P in K]: T; };

它用了 keyof any,难道这个 keyof any 就能动态得到 key 支持的类型么?

我们试一下,不开启 keyofStringsOnly 时:

几个一看就会的 TypeScript 小技巧

开启 keyofStringsOnly 时:

几个一看就会的 TypeScript 小技巧

妙啊,这样就能动态获取当前支持的 key 的类型了。

需要约束某个类型参数为索引 Key 时,用 keyof any 动态获取比写死 string | number | symbol 更好。

object 和 Record<string, any>

TypeScript 里有三个类型比较难区分,就是 object、Object、{} 这几个。

其实只要记住 object 不能接受原始类型 就可以了,其余两个差不多,只不过 {} 是个空对象,没有索引。

所以 number 就可以赋值给 {}、Object 类型,但是不能赋值给 object 类型:

几个一看就会的 TypeScript 小技巧
几个一看就会的 TypeScript 小技巧
几个一看就会的 TypeScript 小技巧

其实,你看源码会发现大家不会用 object 来约束,而是用 Record<string, any> 来约束索引类型,这俩其实是一样的,但是 Record<string, any> 更语义化一些。

Record<string, any> 创建了一个 key 为任意 string,value 为任意类型的索引类型:

几个一看就会的 TypeScript 小技巧

所以,平时约束索引类型的时候就可以用 Record<string, any> 代替 object。

而且你会在很多源码里看到这种写法,比如下面是 Nest.js 源码里的:

几个一看就会的 TypeScript 小技巧

-readonly

映射类型可以构造一个新的索引类型,并且构造的过程中做一些修改。

比如构造一个新的索引类型,把所有的 Key 变为可选:

type ToPartial<T> = { [Key in keyof T]?: T[Key] }
几个一看就会的 TypeScript 小技巧

或者构造一个新的索引类型,加上 readonly 的修饰:

type ToReadonly<T> = { readonly [Key in keyof T]: T[Key]; }
几个一看就会的 TypeScript 小技巧

但很多人不知道也可以去掉已有的修饰的,用 - 号,减去的意思:

比如去掉 ? 是 -? :

type ToRequired<T> = { [Key in keyof T]-?: T[Key] }
几个一看就会的 TypeScript 小技巧

那去掉 readonly 自然就是 -readonly:

type ToMutable<T> = { -readonly [Key in keyof T]: T[Key] }
几个一看就会的 TypeScript 小技巧

我最近看到 Promise.all 的类型定义就用到这个了:

几个一看就会的 TypeScript 小技巧

类型参数 T 是 待处理的 promise 数组,返回值是 Promise 的 value 对应的数组,用 Awaited 取出 value 的类型。

Awaited 是 TS 内置的一个高级类型,用于取出 Promise 返回值类型的:

几个一看就会的 TypeScript 小技巧

返回的是数组类型,那为啥还可以用映射类型的语法呢?

因为数组类型也是索引类型呀,索引类型的意思就是聚合多个元素的类型,数组、对象、class 都是索引类型。

当然,主要还是为了讲 -readonly 的语法,可以去掉 readonly 的修饰。

this

方法里可以调用 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 调用:

几个一看就会的 TypeScript 小技巧

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 指向的对象是否是对的:

几个一看就会的 TypeScript 小技巧

而且,TypeScript 也提供了一个内置的高级类型 ThisParameterType 用于提取 this 的类型:

几个一看就会的 TypeScript 小技巧

它的实现很简单,就是通过模式匹配提取 this 的类型到 infer 声明的局部变量里返回:

? 和 ??

最后是一个比较常用的语法,TS 支持 ? 的可选链语法,也可以通过 ?? 指定默认值:

const dong = data?.name ?? 'dong';

编译之后会变成这样:

做了空值检查,也设置了默认值 dong。

很简单和有用的一个语法,但很多人写 ts 还是没把它用起来。

总结

TypeScript 有很多灵活的语法,小技巧很多。

今天分享了一些大家可能不知道的技巧:

  • keyof any 可以动态获取 key 支持的类型,根据 keyofStringsOnly 的编译选项,可以用来约束索引。

  • object 不能接收原始类型,而 {} 和 Object 都可以,这是它们的区别。

  • object 一般会用 Record<string, any> 代替,约束索引类型更加语义化

  • 映射类型语法可以创建索引类型,并且加上 readonly 或 ? 的修饰,其实也可以用 -readonly、-? 去掉

  • ? 和 ?? 分别代表空判断和默认值,是写 TS 很常用的一个语法

  • this 的类型是可以约束的,而且也可以用内置的高级类型 ThisParameterTypes 来取

这几个小技巧都是看一遍就会的那种,下次写 TS 类型的时候就可以用起来了。