- 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的映射类型。