第五章 高级类型

  • Union Types
  • Type guards
  • Generics
  • Overload signatures
  • Lookup and mapped types

Union types

顾名思义,联合类型就是将类型组合的一种形式。

String literal types

字符串字面量类型,

1
type Control = "Textbox"

这个类型的值仅能是"Textbox"

1
2
let notes: Control;
notes = "Textbox";

用其它值表示则会报错,

1
notes = "DropDown";		// "DropDown" is not assignable to type "Textbox"

和其它TypeScript类型一样,nullundefined是有效的值,

1
2
notes = null;
notes = undefined;

字符串字面量类型自身没有多大用处,它的用处在于结合到联合类型中。

String literal union types

字符串字面量联合类型就是将多个字符串字面类型组合在一起。例如,将原先的Control类型增强为联合类型,

1
type Control = "Textbox" | "DropDown"

设置值为二选一,

1
2
3
let notes: Control;
notes = "Textbox";
notes = "DropDown";

扩展更多的字面量,

1
type Control = "Textbox" | "DropDown" | "DatePicker" | "NumberSlider";

Discriminated union pattern

区分联合模式(discriminated union pattern)允许我们从不同联合类型中处理逻辑,以一个例子说明,

  1. 首先创建三个不同的接口分别表示textbox、date picker、number slider,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ITextbox {
control: "TextBox";
value: string;
multiline: boolean;
}

interface IDatePicker {
control: "DatePicker";
value: Date;
}

interface INumberSlider {
control: "NumberSlider";
value: number;
}

它们都有一个属性control,会成为模式的判别准则,

  1. 我们将这些接口组合成为一个联合类型叫做Field
1
type Field = ITextbox | IDatePicker | INumberSlider;
  1. 接着创建一个函数来初始化Field类型的值,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function intializeValue(field: Field) {
switch (field.control) {
case "Textbox":
field.value = "";
break;
case "DatePicker":
field.value = new Date();
break;
case "NumberSlier":
filed.value = 0;
break;
default:
const shouldNotReach: never = field;
}
}

初始化值的设置取决于这个区分属性control。因此我们需要使用switch语句进行分岔处理。

其中default分支在switch语句中应该从不达到,对于不可达语句,使用never类型表述。

  1. 随着时间的推移,新增了一个checkbox字段需求,接着实现这个接口,
1
2
3
4
interface ICheckbox {
control: "Checkbox";
value: boolean;
}
  1. 将这个field添加到联合Field类型中,
1
type Field = ITextbox | IDatePicker | INumberSlider | ICheckbox;

我们将会立即看到initializeValue函数在never声明中抛出编译错误,

  1. 增加一个分支即可,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function intializeValue(field: Field) {
switch (field.control) {
case "Textbox":
field.value = "";
break;
case "DatePicker":
field.value = new Date();
break;
case "NumberSlider":
field.value = 0;
break;
case "Checkbox":
field.value = false;
break;
default:
const shouldNotReach: never = field;
}
}

因此,联合类型允许我们组合任何类型成为另外一种类型。

Type guards

类型守卫,允许我们在一个代码条件分支上缩小一个对象的具体类型。对于联合类型来说可以在代码分支对不同具体类型进行处理。

例如上面的intializeValue函数,通过switch语句控制分支control来对不同设值类型进行处理。

下面介绍另外一种不同的方式。

Using the typeof keyword

typeof关键字是JavaScript的关键字,它会返回改类型的一个字符串。因此可以使用它缩小类型。

例如,有一个包含字符串和一个字符串数组的类型,

1
type StringOrStringArray = string | string[];

我们需要实现一个first的函数,它接收类型为StringOrStringArray的参数并返回一个字符串,

1
2
function first(stringOrArray: StringOrStringArray): string {
}

要求函数,如果是一个字符串,则返回第一个字符,如果是一个字符串数组则返回数组第一个元素,

1
2
3
4
5
6
7
function first(stringOrArray: StringOrStringArray): string {
if (typeof stringOrArray === "string") {
return stringOrArray.substr(0, 1);
} else {
return stringOrArray[0];
}
}

检测是否生效,

1
2
console.log(first("The"));
console.log(first(["The", "cat"]));

因为typeof关键字仅能被用于JavaScript类型。为了说明这一点,对原来的函数做了增强。

1
2
3
4
5
6
7
8
9
function firstEnhanced(stringOrArray: StringOrStringArray): string {
if (typeof stringOrArray === "string") {
return stringOrArray.substr(0, 1);
} else if (typeof stringOrArray === "string[]") {
return stringOrArray[0];
} else {
const shouldNotReach: never = stringOrArray;
}
}

此时TypeScript编译器在第二个分支报错,typeof关键字仅作用于JavaScript类型,即stringnumberbooleansymbolundefinedobject以及function;错误信息告诉我们string[]类型和JavaScript的类型object重合了,因此第二个分支实际上返回的是object

修改为,

1
2
3
4
5
6
7
8
9
function firstEnhanced(stringOrArray: StringOrStringArray): string {
if (typeof stringOrArray === "string") {
return stringOrArray.substr(0, 1);
} else if (typeof stringOrArray === "object") {
return stringOrArray[0];
} else {
const shouldNotReach: never = stringOrArray;
}
}

因此,typeof对于JavaScript类型是良好的,但对于TypeScript的具体类型却无从入手。

Using the instanceof keyword

instanceof关键字还是JavaScript的,典型地被用于决定一个对象是否是某个类的实例。

例如,有两个类PersonCompany

1
2
3
4
5
6
7
8
9
10
class Person {
id: number;
firstName: string;
surname: string;
}

class company {
id: number;
name: string;
}

以及定义一个联合类型,

1
type PersonOrCompany = Person | Company;

现在编写一个函数,接收一个PersonCompany,并输出名字到控制台,

1
2
3
4
5
6
7
function logName(personOrCompany: PersonOrCompany) {
if (personOrCompany instanceof Person) {
console.log(`${personOrCompany.firstName} ${personOrCompany.surname}`);
} else {
console.log(personOrCompany.name);
}
}

instanceof虽然缩小了类的类型,但它仍然是JavaScript类型,有许多TypeScript类型不能处理。

Using the in keyword

in关键字是另外一个JavaScript关键字,被用于检测一个属性是否是一个对象。

例如,取代原来类的定义,使用接口对PersonCompany进行声明,

1
2
3
4
5
6
7
8
9
10
interface IPerson {
id: number;
firstName: string;
surname: string;
}

interface ICompany {
id: number;
name: string;
}

以及创建一个联合类型,

1
type PersonOrCompany = IPerson | ICompany;

重写原来的方法,

1
2
3
4
5
6
7
function logName(personOrCompany: PersonOrCompany) {
if ("firstName" in personOrCompany) {
console.log(`${personOrCompany.firstName} ${personOrCompany.surname}`);
} else {
console.log(personOrCompany.name);
}
}

in关键字比较灵活,可以被用于任何对象类型。

Using a user-defined type guard

自定义类型守卫,这部分属于TypeScript3特性,改写原来的代码,

1
2
3
4
5
6
7
8
9
10
11
12
interface IPerson {
id: number;
firstName: string;
surname: string;
}

interface ICompany {
id: number;
name: string;
}

type PersonOrCompany = IPerson | ICompany;

然后实现类型守卫函数,返回boolean,

1
2
3
function isPerson(personOrCompany: PersonOrCompany): personOrCompany is IPerson {
return "firstName" in personOrCompany;
}

Generics

泛型可以作用于一个函数或类中。

Generic functions

下面通过一个例子描述泛型函数。我们将创建一个包装函数围绕fetch函数获取web servcie的数据,

  1. 首先创建函数的方法签名,
1
2
function getData<T>(url: string): Promise<T> {
}

如果想要转变为arrow function的形式,我们可以…

1
2
const getData = <T>(url: string): Promise<T> => {
};
  1. 现在让我们实现我们的函数,
1
2
3
4
5
6
7
8
function getData<T>(url: string): Promise<T> {
return fetch(url).then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
});
}
  1. 最后,消费这个函数,
1
2
3
4
5
6
interface IPerson {
id: number;
name: string;
}

getData<IPerson>("/people/1").then(person => console.log(person));

Generic classes

泛型类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class List<T> {
private data: T[] = [];

public getList(): T[] {
return this.data;
}

public add(item: T) {
this.data.push(item);
}

public remove(item: T) {
this.data = this.data.filter(dataItem: T) => {
return !this.equals(item, dataItem);
});
}
private equals(obj1: T, obj2: T) {
return Object.keys(obj1).every(key => {
return obj1[key] === obj2[key];
});
}
}

创建对应的消费接口,

1
2
3
4
5
interface IPerson {
id: number;
name: string;
}
const billy: IPerson = { id: 1, name: "Billy" };

然后创建一个泛型类实例,

1
const people = new List<IPerson>();

调用泛型方法,

1
2
people.add(billy);
people.remove(billy);

获取条目信息,

1
const items = people.getList();

其中React.Component包含有两个泛型参数,分别是props和state。

Overload signatures

方法签名重载。

首先有两个函数,

1
2
3
4
5
6
7
function condenseString(string: string): string {
return string.split(" ").join("");
}

function condenseArray(array: string[]): string[] {
return array.map(item -> item.split(" ").join(""));
}

现在将这两个函数组合为一个函数。我们可以使用联合类型,

1
2
3
function condense(stringOrArray: string | string[]): string| string[] {
return typeof stringOrArray === "string" ? stringOrArray.split(" ").join("") : stringOrArray.map(item => item.split(" ").join(""));
}

调用该函数,

1
const condensedText = condense("the cat sat on the mat");

如果我们将鼠标放在condensedText上,我们会发现它是个联合类型,

现在添加两个重载的方法签名,

1
2
3
function condense(string: string): string;
function condense(array: string[]): string[];
function condense(stringOrArray: string | string[]): string | string[] { ... }

再次消费重载的函数,

1
const moreCondensedText = condense("The cat sat on the mat");

将鼠标悬浮在moreCondensedText上,可以得到一个更好的确定类型是string。可以看到重载方法签名可以得到更好的类型推断。

Lookup and mapped types

TypeScript中提供了一个关键字keyof用于为一个对象中的所有属性创建联合类型。这种被创建的类型被称为查询类型(lookup type)。它允许我们基于已有的类型的属性,动态地创建类型。

以一个例子为例,我们有下面这些接口,

1
2
3
4
interface IPerson {
id: number;
name: string;
}

然后使用关键字keyof创建该接口的查询类型(lookup type),

1
type PersonProps = keyof IPerson;

如果将光标悬浮在PersonProps类型上,我们可以看到它是个联合类型,包含"id""name"属性,

在原来的IPerson上添加一个新的属性,

1
2
3
4
5
interface IPerson {
id: number;
name: string;
age: number
}

现在PersonProps类型包含了一个新的扩展属性"age"

因此PersonProps类型是个lookup type,顾名思义它总是会查询它需要的字面量。

接下来我们看看这种查询类型的某些有用的地方,

1
2
3
4
5
class Field {
name: string;
label: string;
defaultValue: any;
}

这仅是开始,我们可以让name属性更强,并使原来的类是个泛型类,

1
2
3
4
5
class Field<T, K extends keyof T> {
name: K,
label: string;
defaultValue: any;
}

我们在该类创建了两个泛型参数。第一个泛型参数是对象类型,第二个是对象类型的属性类型。

然后我们创建这个类的实例,

1
const idField: Field<IPerson, "id"> = new Field();

尝试引用不存在于IPerson的属性会发生报错,

1
const addressField: Field<IPerson, "address"> = new Field();

另外,defaultValue不是类型安全的,譬如可以设置值为字符串,

1
idField.defaultValue = "2";

可以改为,

1
2
3
4
5
class Field<T, K extends keyof T> {
name: K;
label: string;
defaultValue: T[K];
}

查询T[K]的类型,对于idField。它会处理到IPerson的属性id,即number

1
idFiled.defaultValue = 2;

接下来创建一个映射类型。映射类型就是映射已存在类型的属性。

首先创建一个类型,

1
2
3
4
interface IPerson {
id: number;
name: string;
}

然后创建一个只读版本的新类型,

1
type ReadonlyPerson = { readonly [P in keyof IPerson]: IPerson[P] };

其中重要的是创建这个映射[P in keyof IPerson]。这里将IPerson的所有属性迭代指派给P。因此,这个类型实际上是,

1
2
3
4
type ReadonlyPerson = {
readonly id: number
readonly name: string
}

在visual studio code中,它的实际定义会被解析为,

1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

我们可以尝试创建我们自己呃泛型映射类型,

1
type Stringify<T> = { [P in keyof T]: string };

然后消费我们的映射类型,

1
2
3
4
let tim: Stringify<IPerson> = {
id: "1",
name: "Time"
};

映射类型适用于需要从已有的类型创建一个新的类型的场景。在TypeScript中,除了Radonly<T>类型外,还有Partial<T>,它会创建一个所有属性都是optional的映射类型。