TypeScript 类型运算符
TypeScript 提供强大的类型运算能力,可以使用各种类型运算符
TypeScript 类型运算符
TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。
keyof 运算符
简介
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
上面示例中,keyof MyObj
返回MyObj
的所有键名组成的联合类型,即'foo'|'bar'
。
下面是另一个例子。
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol
。
对于没有自定义键名的类型使用 keyof 运算符,返回never
类型,表示不可能有这样类型的键名。
上面示例中,由于object
类型没有自身的属性,也就没有键名,所以keyof object
返回never
类型。
由于 keyof 返回的类型是string|number|symbol
,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。
上面示例中,类型Capital
只接受字符串作为类型参数,传入keyof Obj
会报错,原因是这时的类型参数是string|number|symbol
,跟字符串不兼容。采用下面的交叉类型写法,就不会报错。
上面示例中,string & keyof Obj
等同于string & string|number|symbol
进行交集运算,最后返回string
,因此Capital<T extends string>
就不会报错了。
如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。
上面的示例二,keyof T
返回的类型是string|number
,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。
如果 keyof 运算符用于数组或元组类型,得到的结果可能出人意料。
上面示例中,keyof 会返回数组的所有键名,包括数字键名和继承的键名。
对于联合类型,keyof 返回成员共有的键名。
对于交叉类型,keyof 返回所有键名。
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。
上面示例中,Keys
是键名组成的联合类型,而MyObj[Keys]
会取出每个键名对应的键值类型,组成一个新的联合类型,即number|string
。
keyof 运算符的用途
keyof 运算符往往用于精确表达对象的属性类型。
举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。
上面这个函数添加类型,只能写成下面这样。
上面的类型声明有两个问题,一是无法表示参数key
与参数obj
之间的关系,二是返回值类型只能写成any
。
有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。
上面示例中,K extends keyof Obj
表示K
是Obj
的一个属性名,传入其他字符串会报错。返回值类型Obj[K]
就表示K
这个属性值的类型。
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
上面示例中,类型NewProps
是类型Obj
的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了boolean
。
下面的例子是去掉 readonly 修饰符。
上面示例中,[Prop in keyof Obj]
是Obj
类型的所有属性名,-readonly
表示去除这些属性的只读特性。对应地,还有+readonly
的写法,表示添加只读属性设置。
下面的例子是让可选属性变成必有的属性。
上面示例中,[Prop in keyof Obj]
后面的-?
表示去除可选属性设置。对应地,还有+?
的写法,表示添加可选属性设置。
in 运算符
JavaScript 语言中,in
运算符用来确定对象是否包含某个属性名。
上面示例中,in
运算符用来判断对象obj
是否包含属性a
。
in
运算符的左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。
TypeScript 语言的类型运算中,in
运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
上面示例中,[Prop in U]
表示依次取出联合类型U
的每一个成员。
上一小节的例子也提到,[Prop in keyof Obj]
表示取出对象Obj
的每一个键名。
方括号运算符
方括号运算符([]
)用于取出对象的键值类型,比如T[K]
会返回对象T
的属性K
的类型。
上面示例中,Person['age']
返回属性age
的类型,本例是number
。
方括号的参数如果是联合类型,那么返回的也是联合类型。
上面示例中,方括号里面是属性名的联合类型,所以返回的也是对应的属性值的联合类型。
如果访问不存在的属性,会报错。
方括号运算符的参数也可以是属性名的索引类型。
上面示例中,Obj
的属性名是字符串的索引类型,所以可以写成Obj[string]
,代表所有字符串属性名,返回的就是它们的类型number
。
这个语法对于数组也适用,可以使用number
作为方括号的参数。
上面示例中,MyArray
是一个数组,它的类型实际上是属性名的数值索引,而typeof MyArray[number]
的typeof
运算优先级高于方括号,所以返回的是所有数值键名的键值类型string
。
注意,方括号里面不能有值的运算。
上面两个示例,方括号里面都涉及值的运算,编译时不会进行这种运算,所以会报错。
extends...?: 条件运算符
TypeScript 提供类似 JavaScript 的?:
运算符这样的三元运算符,但多出了一个extends
关键字。
条件运算符extends...?:
可以根据当前类型是否符合某种条件,返回不同的类型。
上面式子中的extends
用来判断,类型T
是否可以赋值给类型U
,即T
是否为U
的子类型,这里的T
和U
可以是任意类型。
如果T
能够赋值给类型U
,表达式的结果为类型X
,否则结果为类型Y
。
上面示例中,1
是number
的子类型,所以返回true
。
下面是另外一个例子。
上面示例中,Dog
是Animal
的子类型,所以T1
的类型是number
。RegExp
不是Animal
的子类型,所以T2
的类型是string
。
一般来说,调换extends
两侧类型,会返回相反的结果。举例来说,有两个类Cat
和Animal
,前者是后者的子类型,那么Cat extends Animal
就为真,而Animal extends Cat
就为伪。
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。
上面示例中,A|B
是一个联合类型,进行条件运算时,相当于A
和B
分别进行运算符,返回结果组成一个联合类型。
如果不希望联合类型被条件运算符展开,可以把extends
两侧的操作数都放在方括号里面。
上面的示例一,传入ToArray<Type>
的类型参数是一个联合类型,所以会被展开,返回的也是联合类型。示例二是extends
两侧的运算数都放在方括号里面,所以传入的联合类型不会展开,返回的是一个数组。
条件运算符还可以嵌套使用。
上面示例是一个多重判断,返回一个字符串的值类型,对应当前类型。下面是它的用法。
infer 关键字
infer
关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在extends
关键字后面的父类型之中。
上面示例中,infer Item
表示Item
这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten<Type>
则表示Type
这个类型参数是外部传入的。Type extends Array<infer Item>
则表示,如果参数Type
是一个数组,那么就将该数组的成员类型推断为Item
,即Item
是从Type
推断出来的。
一旦使用Infer Item
定义了Item
,后面的代码就可以直接调用Item
了。下面是上例的泛型Flatten<Type>
的用法。
上面示例中,第一个例子Flatten<string[]>
传入的类型参数是string[]
,可以推断出Item
的类型是string
,所以返回的是string
。第二个例子Flatten<number>
传入的类型参数是number
,它不是数组,所以直接返回自身。
如果不用infer
定义类型参数,那么就要传入两个类型参数。
上面是不使用infer
的写法,每次调用Flatten
的时候,都要传入两个参数,就比较麻烦。
下面的例子使用infer
,推断函数的参数类型和返回值类型。
上面示例中,如果T
是函数,就返回这个函数的 Promise 版本,否则原样返回。infer A
表示该函数的参数类型为A
,infer R
表示该函数的返回值类型为R
。
如果不使用infer
,就不得不把ReturnPromise<T>
写成ReturnPromise<T, A, R>
,这样就很麻烦,相当于开发者必须人肉推断编译器可以完成的工作。
下面是infer
提取对象指定属性的例子。
上面示例中,infer
提取了参数对象的属性a
和属性b
的类型。
下面是infer
通过正则匹配提取类型参数的例子。
上面示例中,rest
是从模板字符串提取的类型参数。
is 运算符
函数返回布尔值的时候,可以使用is
运算符,限定返回值与参数之间的关系。
is
运算符用来描述返回值属于true
还是false
。
上面示例中,函数isFish()
的返回值类型为pet is Fish
,表示如果参数pet
类型为Fish
,则返回true
,否则返回false
。
is
运算符总是用于描述函数的返回值类型,写法采用parameterName is Type
的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。
上面示例中,返回值类型x is A
可以准确描述函数体内部的运算逻辑。
is
运算符可以用于类型保护。
上面示例中,函数isCat()
的返回类型是a is Cat
,它是一个布尔值。后面的if
语句就用这个返回值进行判断,从而起到类型保护的作用,确保x
是 Cat 类型,从而x.meow()
不会报错(假定Cat
类型拥有meow()
方法)。
is
运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值。
上面示例中,isStudent()
方法的返回值类型,取决于该方法内部的this
是否为Student
对象。如果是的,就返回布尔值true
,否则返回false
。
注意,this is T
这种写法,只能用来描述方法的返回值类型,而不能用来描述属性的类型。
模板字符串
TypeScript 允许使用模板字符串,构建类型。
模板字符串的最大特点,就是内部可以引用其他类型。
上面示例中,类型Greeting
是一个模板字符串,里面引用了另一个字符串类型world
,因此Greeting
实际上是字符串hello world
。
注意,模板字符串可以引用的类型一共7种,分别是 string、number、bigint、boolean、null、undefined、Enum。引用这7种以外的类型会报错。
上面示例中,模板字符串引用数值类型的别名Num
是可以的,但是引用对象类型的别名Obj
就会报错。
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。
上面示例中,类型U
是一个模板字符串,里面引用了一个联合类型T
,导致最后得到的也是一个联合类型。
如果模板字符串引用两个联合类型,它会交叉展开这两个类型。
上面示例中,T
和U
都是联合类型,各自有两个成员,模板字符串里面引用了这两个类型,最后得到的就是一个4个成员的联合类型。
satisfies 运算符
satisfies
运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用satisfies
运算符对其进行检测。TypeScript 4.9添加了这个运算符。
举例来说,有一个对象的属性名拼写错误。
上面示例中,对象palette
的属性名拼写错了,将blue
拼成了bleu
,我们希望通过指定类型,发现这个错误。
上面示例中,变量palette
的类型被指定为Record<Colors, string|RGB>
,这是一个类型工具,用来返回一个对象,详细介绍见《类型工具》一章。简单说,它的第一个类型参数指定对象的属性名,第二个类型参数指定对象的属性值。
本例的Record<Colors, string|RGB>
,就表示变量palette
的属性名应该符合类型Colors
,属性值应该符合类型string|RGB
,要么是字符串,要么是元组RGB
。属性名bleu
不符合类型Colors
,所以就报错了。
这样的写法,虽然可以发现属性名的拼写错误,但是带来了新的问题。
上面示例中,palette.green
属性调用substring()
方法会报错,原因是这个方法只有字符串才有,而palette.green
的类型是srting|RGB
,除了字符串,还可能是元组RGB
,而元组并不存在substring()
方法,所以报错了。
如果要避免报错,要么精确给出变量palette
每个属性的类型,要么对palette.green
的值进行类型缩小。两种做法都比较麻烦,也不是很有必要。
这时就可以使用satisfies
运算符,对palette
进行类型检测,但是不改变 TypeScript 对palette
的类型推断。
上面示例中,变量palette
的值后面增加了satisfies Record<Colors, string|RGB>
,表示该值必须满足Record<Colors, string|RGB>
这个条件,所以能够检测出属性名bleu
的拼写错误。同时,它不会改变palette
的类型推断,所以,TypeScript 知道palette.green
是一个字符串,对其调用substring()
方法就不会报错。
satisfies
也可以检测属性值。
上面示例中,属性blue
的值只有两个成员,不符合元组RGB
必须有三个成员的条件,从而报错了。