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:
这还与编译选项有关,那这里改怎么约束呢?
看下 TS 源码里是怎么定义 Record 的:
type Record<K extends keyof any, T> = { [P in K]: T; };
它用了 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] }
或者构造一个新的索引类型,加上 readonly 的修饰:
type ToReadonly = { readonly [Key in keyof T]: T[Key]; }
但很多人不知道也可以去掉已有的修饰的,用 - 号,减去的意思:
比如去掉 ? 是 -? :
type ToRequired<T> = { [Key in keyof T]-?: T[Key] }
那去掉 readonly 自然就是 -readonly:
type ToMutable<T> = { -readonly [Key in keyof T]: T[Key] }
我最近看到 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();
用对象.方法名的方式调用的时候,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 指向的对象是否是对的:
而且,TypeScript 也提供了一个内置的高级类型 ThisParameterType 用于提取 this 的类型:
它的实现很简单,就是通过模式匹配提取 this 的类型到 infer 声明的局部变量里返回:
? 和 ??
最后是一个比较常用的语法,TS 支持 ? 的可选链语法,也可以通过 ?? 指定默认值:
const dong = data?.name ?? 'dong';
编译之后会变成这样:
做了空值检查,也设置了默认值 dong。
很简单和有用的一个语法,但很多人写 ts 还是没把它用起来。
总结
TypeScript 有很多灵活的语法,小技巧很多。
今天分享了一些大家可能不知道的技巧:
- keyof any 可以动态获取 key 支持的类型,根据 keyofStringsOnly 的编译选项,可以用来约束索引。
- object 不能接收原始类型,而 {} 和 Object 都可以,这是它们的区别。
- object 一般会用 Record 代替,约束索引类型更加语义化。
- 映射类型语法可以创建索引类型,并且加上 readonly 或 ? 的修饰,其实也可以用 -readonly、-? 去掉。
- ? 和 ?? 分别代表空判断和默认值,是写 TS 很常用的一个语法。
- this 的类型是可以约束的,而且也可以用内置的高级类型 ThisParameterTypes 来取。
这几个小技巧都是看一遍就会的那种,下次写 TS 类型的时候就可以用起来了。