TypeScript系列基础篇(四) 类型操纵
TypeScript 系列基础篇(四) 类型操纵
定义一个类型,我们通常使用interface
和 type
关键字来进行规定,有时候也会直接使用字面量类型,这些过程足以应付大部分场景。但是有些时候,我们希望掌握从已经存在的值或者类型中提取或派生出新的类型的技巧,这就是我们今天要来了解的Type Manipulation
类型操纵。相信我,掌握这些技巧后,你也能被各种类型玩出百般花样。
一、使用 keyof
操作符
使用 keyof
操作符,可以获取一个 对象类型 的属性名 (字符串或者数值),并将其组合成一个联合类型。注意得到的是一个类型,因此应使用type
关键字,而不能使用var
, let
, const
等声明值变量的关键字。
- 一般情况下,
keyof
操作会得到字面量联合类型;
1 | // Person类型有三个属性,属性名分别为"name","age",100 |
如果被操作的对象类型有着
string
类型或者number
类型的索引签名,那么keyof
操作会得到string
或者number
类型,而不是字面量类型。number
类型的索引签名,keyof
会得到number
类型;string
类型的索引签名,keyof
会得到string | number
类型;
1 | // 索引签名规定索引必须为number类型,属性值是string类型 |
由于JavaScript
中对象的 key
会被强制转换成 string
类型。因此,我们即使用数值型的key
,最后也等同于转化之后的字符串 key
,即 obj[0]
和 obj["0"]
完全一样。因此,string
类型的索引签名,对象实例可以由 number
类型的key
,因为会被强制转化为string
。所以keyof
操作得到的是 string | number
类型。
二、使用 typeof
操作符
在JavaScript
中,typeof
操作符常用于基本数据类型的判断。而TS
在类型上下文中也加入了 typeof
操作符,用于获取一个变量或属性的类型。不同于我们平常的用法,当typeof
出现在类型上下文中时,得到的类型也可以是一个对象类型。
1 | let a = 100; |
typeof
用在这些简单类型的值上,不得不说有点累赘。但是对于复杂类型的值,用typeof
就可以很方便地表达多种类型。例如,可以用TS
提供的泛型类型ReturnType
来获取一个函数类型的返回值的类型(指定ReturnType
的类型参数为某个函数类型,得到该函数类型的返回值的类型)。
1 | // 泛型函数类型表达式,设置泛型类型参数默认值 |
注意 ReturnType
接收的泛型类型参数应该是一个类型,而不是一个值。在类型上下文中可以使用typeof
关键字将通过表示值的变量或属性来表达一个类型。
1 | // fn是"值" |
注意,typeof
关键字在类型上下文中,永远只能用于变量名和属性名后面。在平常用于判断类型时,则不受此限制。类型上下文:接收一个类型作为参数或者声明、表达、生成一个类型的上下文,如interface
、type
等关键字以及泛型参数的上下文。
三、索引访问类型
在对象实例中,我们可以通过索引来访问某个属性值。同样的,在对象类型中,我们也可以通过索引访问某个属性的类型,此时的索引应该是一个类型,而不是一个值,用方括号的形式接收该索引。索引可以是联合类型、字面量类型、类型别名、乃至keyof
操作符表达的类型
1 | // 定义一个对象类型 |
无法访问对象类型中没有的属性。
1 | // 定义一个对象类型 |
前面说到,接收的索引应该是一个类型。所以,当我们的对象类型具有索引签名时,则也可以接收string
、number
。另外,在数组类型中,也可以用number
作为索引来访问。
1 | interface Person { |
四、条件类型
类似我们常用的三元表达式,条件类型表达式:Type1 extends Type2 ? TrueType : FalseType
;当Type2
是Type1
的子类型时,表达式得到TrueType
,否则得到FalseType
。这看起来好像没啥用,然而,
- 条件类型表达式的强劲之处在于用于泛型。
1 | type MyType<T> = T extends { info: unknown } ? T["info"] : never; |
对条件类型的泛型类型参数使用extends
关键字可以约束其类型:
- 在条件类型中进行推论
在条件类型表达式的中使用 infer
关键字推论泛型类型参数或函数类型的返回值的类型,并用一个形式类型指代,这个形式类型可用于条件类型表达式中指代推论得出的实际类型。
1 | // 在条件类型表达式的Array泛型中使用infer关键字, |
- 分布式条件类型
当我们为泛型指定的类型参数为联合类型时,条件类型的作用会分布于联合类型的每一个单独的子类型上。
1 | type GetArrType<T> = T extends any ? T[] : never; |
五、映射类型
1. 基本使用
映射类型是一种泛型,往往建立在索引签名之上,即以索引签名的形式,利用指定的泛型类型参数T
的所有属性类型的联合(一般使用 keyof
关键字来遍历出T
的所有属性类型),作为新的对象类型的索引签名,并为其指定新的返回值类型。好吧,这太绕了。说白了,就是给对象类型 A 的所有属性/方法指定新的返回值的类型,从而得到一个新的对象类型。关键字 in
后面是一个与类型参数T
有关的联合类型。还是有些绕?那就来看一个栗子吧:
1 | type OptionsFlags<Type> = { |
这下我们就能明白了,文字说再多都是虚的,还得是代码。通过映射类型的方式得到新的类型,新类型继承了所有原来的属性(包括方法),并指定了新的返回值 (当然可以使用条件类型表达式)。
2.映射修饰符
既然已经了解了映射类型的基本使用,现在来看看有哪些类型修饰符,它们又分别是用来做什么的。在上一篇文章今天来聊聊 TS 中的那些对象类型——TypeScript 系列:(三) 对象类型中,我们了解了对象属性的属性修饰符 ?
和 readonly
,事实上,映射修饰符也是这两小只。
readonly
修饰符我们知道,通过映射类型,我们可以继承原有对象类型的所有属性,并它们指定新的返回值的类型。那么,我们如何为新的对象类型的属性添加为只读属性呢?或者如果原有的类型中存在只读属性,如何在新的类型中移除只读限制呢?很简单,在签名的
[ ]
之前使用+readonly
、-readonly
号即可,+ 号也可以省略。
1 | // Person类型中有只读 |
?
可选修饰符
同样,在映射类型产生新类型时,可选修饰符也会保留。我们可以在签名的 [ ]
后用 -?
来移除可选性。
1 | type NewType<T> = { |
3. key
的重映射
使用 as
可以在新的对象类型中对原有的 key
进行重映射。直接show code
吧,来一道官方栗子:
1 | type Getters<Type> = { |
在上面的栗子中,我们把那句代码分为三个部分, “as
“ 之前的A
部分, “as
“ ~ “:
“ 之间的B
部分,”:
“之后的C
部分。A
和 C
两部分结合起来,就是我们之前了解到的映射类型。所以,难点在于理解 B
部分的内容。这里涉及到了模板字面量类型,类似与模板字符串,(下一节有详细介绍)。Capitalize
是TS
提供的首字母大写的泛型类型。类型参数Property
指代keyof
每一次遍历到的类型Type
的key
,使用 as
将 新的对象类型中对应的 key
重命名为 模板字面量类型 get + Property
类型(即原来的对象类型的对应的key
)中属于string
类型的key
(即排除number
索引) 首字母大写的 。
还可以利用TS
提供Exclude<P, B>
的泛型类型来排除 B 类型。
1 | // 新类型中移除了"kind"属性 |
还记得一开始我们说的关键字 in
后面是一个与类型参数T
有关的联合类型吗?事实上,这个联合类型不是仅仅只能用简单类型的联合,而可以是一切类型的联合,只需要通过 as
来把 key
重新映射为string
或number
或两者的字面量类型即可。
1 | type EventConfig<Events extends { kind: string }> = { |
上面是一个官网的栗子。Events
是一个具kind
属性的对象类型的联合类型,E
代表这个联合类型中的每一个具有kind
属性的对象类型,也是新对象的key
,通过as
关键字将 E
重映射为索引访问类型 E["kind"]
,也就是string
类型。因此,最后得到的是一个 key
为string
类型的方法签名(函数的调用签名)。
(不得不说,各种类型操纵方法结合起来,是真能玩出花来。虽然明白比尔写的是啥,但我这脑瓜子实在是设计不出来这么优雅的类型。)
六、模板字面量类型
这是本文要分享的最后一种类型操纵方式了,毕竟没把泛型加进来讲,因为我之前的文章里介绍过了。
- 利用字符串模板的形式,可以得到模板字面量类型。注意类型是用
type
来定义。
1 | type Name = "cc"; // 类型,不是值,不要使用let、var、const等 |
- 如果模板用的类型是联合类型,则会分别对联合类型的每一个类型进行处理
1 | type Name = "cc" | "yy"; |
- 如果模板本身也是联合类型,则会分别对模板的每一个类型和使用的类型进行处理
1 | type Name = "cc" | "yy"; |
- 如果有多个模板,则是将每个模板的情况和其它模板的情况进行组合
1 | type Name = "cc" | "yy"; |
- 用在函数签名中,才能体会到模版字面量类型有多强大
1 | type PropEventSource<Type> = { |
- 模板字面量类型的推论
把on( )
设计为泛型函数,使TS
自己进行类型推论。不说了,都是泪。直接上官方示例代码吧。一层一层的泛型,不思考的话实在不容易看懂。
1 | type PropEventSource<Type> = { |
七、内置的字符串操纵类型
1. Uppercase <StringType>
产生一个将泛型类型(必须是字符串类型、字符串型的字面量类型)所有字母转化为大写的新类型。不改变原来的类型。如果是string
类型而不是字面量类型,则生成的类型依然是string
,不要求其值的首字母大写(一下几个内置方法都是如此)。
1 | type Name = "cc" | "yy"; |
2. Lowercase <StringType>
产生一个将泛型类型(必须是字符串类型、字符串型的字面量类型)所有字母转化为小写的新类型。不改变原来的类型。
1 | type Name = "CC" | "YY"; |
3. Capitalize <StringType>
产生一个将泛型类型(必须是字符串类型、字符串型的字面量类型)首字母转化为大写的新类型。不改变原来的类型。
1 | type Name = "cc" | "yy"; |
4. Uncapitalize <StringType>
产生一个将泛型类型(必须是字符串类型、字符串型的字面量类型)首字母转化为小写的新类型。不改变原来的类型。
1 | type Name = "CC" | "YY"; |
好了,本篇就到此为止了,各种类型已经把我玩出花来了。下一篇,我们将一起学习 class
,不见不散!