深圳幻海软件技术有限公司 欢迎您!

几个一看就会的 TypeScript 小技巧

2023-02-28

TypeScript是一门语言,有很多语法,和那些只需要熟悉下API的库的层次不太一样,它更灵活,当然也会有很多小技巧。这篇文章就来分享一些很多人不知道的小技巧吧,都是学完就能用起来的那种。keyofanyTypeScript有一个内置类型叫做Record,它的作用是根据传入的索引和值的类型构造新的

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

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

keyof any

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

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

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

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

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

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

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

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

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

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

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

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

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

开启 keyofStringsOnly 时:

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

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

object 和 Record<string, any>

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

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

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


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

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

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

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

-readonly

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

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

type ToPartial<T> = { [Key in keyof T]?: T[Key] }
  • 1.

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

type ToReadonly = { readonly [Key in keyof T]: T[Key]; }
  • 1.

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

比如去掉 ? 是 -? :

type ToRequired<T> = { [Key in keyof T]-?: T[Key] }
  • 1.

那去掉 readonly 自然就是 -readonly:

type ToMutable<T> = { -readonly [Key in keyof T]: T[Key] }
  • 1.

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

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

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

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

因为数组类型也是索引类型呀,索引类型的意思就是聚合多个元素的类型,数组、对象、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();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

用对象.方法名的方式调用的时候,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;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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


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

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

? 和 ??

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

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

编译之后会变成这样:

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

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

总结

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

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

  • keyof any 可以动态获取 key 支持的类型,根据 keyofStringsOnly 的编译选项,可以用来约束索引。
  • object 不能接收原始类型,而 {} 和 Object 都可以,这是它们的区别。
  • object 一般会用 Record 代替,约束索引类型更加语义化
  • 映射类型语法可以创建索引类型,并且加上 readonly 或 ? 的修饰,其实也可以用 -readonly、-? 去掉
  • ? 和 ?? 分别代表空判断和默认值,是写 TS 很常用的一个语法
  • this 的类型是可以约束的,而且也可以用内置的高级类型 ThisParameterTypes 来取

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