- Union Types
- Type guards
- Generics
- Overload signatures
- Lookup and mapped types
¶Union types
顾名思义,联合类型就是将类型组合的一种形式。
¶String literal types
字符串字面量类型,
1 | type Control = "Textbox" |
这个类型的值仅能是"Textbox",
1 | let notes: Control; |
用其它值表示则会报错,
1 | notes = "DropDown"; // "DropDown" is not assignable to type "Textbox" |
和其它TypeScript类型一样,null和undefined是有效的值,
1 | notes = null; |
字符串字面量类型自身没有多大用处,它的用处在于结合到联合类型中。
¶String literal union types
字符串字面量联合类型就是将多个字符串字面类型组合在一起。例如,将原先的Control类型增强为联合类型,
1 | type Control = "Textbox" | "DropDown" |
设置值为二选一,
1 | let notes: Control; |
扩展更多的字面量,
1 | type Control = "Textbox" | "DropDown" | "DatePicker" | "NumberSlider"; |
¶Discriminated union pattern
区分联合模式(discriminated union pattern)允许我们从不同联合类型中处理逻辑,以一个例子说明,
- 首先创建三个不同的接口分别表示textbox、date picker、number slider,
1 | interface ITextbox { |
它们都有一个属性control,会成为模式的判别准则,
- 我们将这些接口组合成为一个联合类型叫做
Field,
1 | type Field = ITextbox | IDatePicker | INumberSlider; |
- 接着创建一个函数来初始化
Field类型的值,
1 | function intializeValue(field: Field) { |
初始化值的设置取决于这个区分属性control。因此我们需要使用switch语句进行分岔处理。
其中default分支在switch语句中应该从不达到,对于不可达语句,使用never类型表述。
- 随着时间的推移,新增了一个checkbox字段需求,接着实现这个接口,
1 | interface ICheckbox { |
- 将这个field添加到联合
Field类型中,
1 | type Field = ITextbox | IDatePicker | INumberSlider | ICheckbox; |
我们将会立即看到initializeValue函数在never声明中抛出编译错误,
- 增加一个分支即可,
1 | function intializeValue(field: Field) { |
因此,联合类型允许我们组合任何类型成为另外一种类型。
¶Type guards
类型守卫,允许我们在一个代码条件分支上缩小一个对象的具体类型。对于联合类型来说可以在代码分支对不同具体类型进行处理。
例如上面的intializeValue函数,通过switch语句控制分支control来对不同设值类型进行处理。
下面介绍另外一种不同的方式。
¶Using the typeof keyword
typeof关键字是JavaScript的关键字,它会返回改类型的一个字符串。因此可以使用它缩小类型。
例如,有一个包含字符串和一个字符串数组的类型,
1 | type StringOrStringArray = string | string[]; |
我们需要实现一个first的函数,它接收类型为StringOrStringArray的参数并返回一个字符串,
1 | function first(stringOrArray: StringOrStringArray): string { |
要求函数,如果是一个字符串,则返回第一个字符,如果是一个字符串数组则返回数组第一个元素,
1 | function first(stringOrArray: StringOrStringArray): string { |
检测是否生效,
1 | console.log(first("The")); |
因为typeof关键字仅能被用于JavaScript类型。为了说明这一点,对原来的函数做了增强。
1 | function firstEnhanced(stringOrArray: StringOrStringArray): string { |
此时TypeScript编译器在第二个分支报错,typeof关键字仅作用于JavaScript类型,即string、number、boolean、symbol、undefined、object以及function;错误信息告诉我们string[]类型和JavaScript的类型object重合了,因此第二个分支实际上返回的是object。
修改为,
1 | function firstEnhanced(stringOrArray: StringOrStringArray): string { |
因此,typeof对于JavaScript类型是良好的,但对于TypeScript的具体类型却无从入手。
¶Using the instanceof keyword
instanceof关键字还是JavaScript的,典型地被用于决定一个对象是否是某个类的实例。
例如,有两个类Person和Company,
1 | class Person { |
以及定义一个联合类型,
1 | type PersonOrCompany = Person | Company; |
现在编写一个函数,接收一个Person或Company,并输出名字到控制台,
1 | function logName(personOrCompany: PersonOrCompany) { |
instanceof虽然缩小了类的类型,但它仍然是JavaScript类型,有许多TypeScript类型不能处理。
¶Using the in keyword
in关键字是另外一个JavaScript关键字,被用于检测一个属性是否是一个对象。
例如,取代原来类的定义,使用接口对Person和Company进行声明,
1 | interface IPerson { |
以及创建一个联合类型,
1 | type PersonOrCompany = IPerson | ICompany; |
重写原来的方法,
1 | function logName(personOrCompany: PersonOrCompany) { |
in关键字比较灵活,可以被用于任何对象类型。
¶Using a user-defined type guard
自定义类型守卫,这部分属于TypeScript3特性,改写原来的代码,
1 | interface IPerson { |
然后实现类型守卫函数,返回boolean,
1 | function isPerson(personOrCompany: PersonOrCompany): personOrCompany is IPerson { |
¶Generics
泛型可以作用于一个函数或类中。
¶Generic functions
下面通过一个例子描述泛型函数。我们将创建一个包装函数围绕fetch函数获取web servcie的数据,
- 首先创建函数的方法签名,
1 | function getData<T>(url: string): Promise<T> { |
如果想要转变为arrow function的形式,我们可以…
1 | const getData = <T>(url: string): Promise<T> => { |
- 现在让我们实现我们的函数,
1 | function getData<T>(url: string): Promise<T> { |
- 最后,消费这个函数,
1 | interface IPerson { |
¶Generic classes
泛型类,
1 | class List<T> { |
创建对应的消费接口,
1 | interface IPerson { |
然后创建一个泛型类实例,
1 | const people = new List<IPerson>(); |
调用泛型方法,
1 | people.add(billy); |
获取条目信息,
1 | const items = people.getList(); |
其中React.Component包含有两个泛型参数,分别是props和state。
¶Overload signatures
方法签名重载。
首先有两个函数,
1 | function condenseString(string: string): string { |
现在将这两个函数组合为一个函数。我们可以使用联合类型,
1 | function condense(stringOrArray: string | string[]): string| string[] { |
调用该函数,
1 | const condensedText = condense("the cat sat on the mat"); |
如果我们将鼠标放在condensedText上,我们会发现它是个联合类型,
现在添加两个重载的方法签名,
1 | function condense(string: string): string; |
再次消费重载的函数,
1 | const moreCondensedText = condense("The cat sat on the mat"); |
将鼠标悬浮在moreCondensedText上,可以得到一个更好的确定类型是string。可以看到重载方法签名可以得到更好的类型推断。
¶Lookup and mapped types
TypeScript中提供了一个关键字keyof用于为一个对象中的所有属性创建联合类型。这种被创建的类型被称为查询类型(lookup type)。它允许我们基于已有的类型的属性,动态地创建类型。
以一个例子为例,我们有下面这些接口,
1 | interface IPerson { |
然后使用关键字keyof创建该接口的查询类型(lookup type),
1 | type PersonProps = keyof IPerson; |
如果将光标悬浮在PersonProps类型上,我们可以看到它是个联合类型,包含"id"和"name"属性,
在原来的IPerson上添加一个新的属性,
1 | interface IPerson { |
现在PersonProps类型包含了一个新的扩展属性"age",
因此PersonProps类型是个lookup type,顾名思义它总是会查询它需要的字面量。
接下来我们看看这种查询类型的某些有用的地方,
1 | class Field { |
这仅是开始,我们可以让name属性更强,并使原来的类是个泛型类,
1 | class Field<T, K extends keyof T> { |
我们在该类创建了两个泛型参数。第一个泛型参数是对象类型,第二个是对象类型的属性类型。
然后我们创建这个类的实例,
1 | const idField: Field<IPerson, "id"> = new Field(); |
尝试引用不存在于IPerson的属性会发生报错,
1 | const addressField: Field<IPerson, "address"> = new Field(); |
另外,defaultValue不是类型安全的,譬如可以设置值为字符串,
1 | idField.defaultValue = "2"; |
可以改为,
1 | class Field<T, K extends keyof T> { |
查询T[K]的类型,对于idField。它会处理到IPerson的属性id,即number。
1 | idFiled.defaultValue = 2; |
接下来创建一个映射类型。映射类型就是映射已存在类型的属性。
首先创建一个类型,
1 | interface IPerson { |
然后创建一个只读版本的新类型,
1 | type ReadonlyPerson = { readonly [P in keyof IPerson]: IPerson[P] }; |
其中重要的是创建这个映射[P in keyof IPerson]。这里将IPerson的所有属性迭代指派给P。因此,这个类型实际上是,
1 | type ReadonlyPerson = { |
在visual studio code中,它的实际定义会被解析为,
1 | type Readonly<T> = { |
我们可以尝试创建我们自己呃泛型映射类型,
1 | type Stringify<T> = { [P in keyof T]: string }; |
然后消费我们的映射类型,
1 | let tim: Stringify<IPerson> = { |
映射类型适用于需要从已有的类型创建一个新的类型的场景。在TypeScript中,除了Radonly<T>类型外,还有Partial<T>,它会创建一个所有属性都是optional的映射类型。