类型创建


# 类型创建

所谓类型创建,就是基于已有类型,创建出新的类型。创建新类型需要用到的工具有类型别名、交叉类型、索引类型与映射类型。

# 类型别名

类型别名的作用主要是对一组类型或一个特定类型结构进行封装,以便于在其它地方进行复用。

语法是使用 type 关键字,例如:

type StatusCode = 200 | 301 | 400 | 500 | 502;

const status: StatusCode = 502;
1
2
3

# 工具类型

类型别名结合泛型,就成了工具类型。

工具类型能够接受泛型参数(就像函数的参数),然后内部逻辑再基于入参进行某些操作,再返回一个新的类型。

比如这样:

// 定义一个工具类型,返回一个包括 null 的联合类型
type MaybeNull<T> = T | null;

// 确保能处理可能为空值的属性读取与方法调用
function process(input: MaybeNull<{ handler: () => {} }>) {
    // 因为可能是 null,所以用可选链操作符来调用一个可能不存在的方法
    input?.handler();
}
1
2
3
4
5
6
7
8

总之对于工具类型来说,它的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。

# 联合类型与交叉类型

联合类型的符号是 |,它代表了「或」,即只需要符合其中的一个类型,就可以认为实现了这个联合类型,例如:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
1
2
3

交叉类型的符号是 &,它代表了「与」,即需要符合这里的所有类型,才可以说实现了这个交叉类型,例如:

interface NameStruct {
  name: string;
}

interface AgeStruct {
  age: number;
}

type ProfileStruct = NameStruct & AgeStruct;

const profile: ProfileStruct = {
  name: "张三",
  age: 13
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面是对于对象类型的合并,如果是对原始类型进行合并,就会变成 never,因为会造出一个根本不存在的类型:

// 世界上不存在既是 string 又是 number 的类型,所以是 never
type StrAndNum = string & number;
1
2

# 索引类型

索引类型指的不是某一个特定的类型工具,它包含三个部分:索引签名类型、索引类型查询与索引类型访问。这三者都是独立的类型工具,唯一共同点是它们都通过索引的形式来进行类型操作。但索引签名类型是声明,后两者则是读取。

# 索引签名类型

索引签名类型,主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构

// 用在接口中
interface AllStringTypes {
  [key: string]: string;
}

// 用在类型别名中
type AllStringTypes = {
  [key: string]: string;
}
1
2
3
4
5
6
7
8
9

这时即使还没声明具体的属性,也意味着在实现这个类型结构的变量中只能声明字符串类型的键

interface AllStringTypes {
  [key: string]: string;
}

const foo: AllStringTypes = {
  "name": "张三"
}
1
2
3
4
5
6
7

# 索引类型查询

索引类型查询,可以通过 keyof 操作符,将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。

interface Foo {
  name: string,
  age: number,
  123: 13
}

type FooKeys = keyof Foo; // "name" | "age" | 123

const tmp: FooKeys = 123  // tmp 的值只能是 "name"、"age"、123 中的一个
1
2
3
4
5
6
7
8
9

keyof 的产物必定是一个联合类型,而 keyof any 产生的联合类型,是由所有可用作对象键值的类型组成的,即 string | number | symbol

# 索引类型访问

在 JavaScript 中我们可以通过 obj[expression] 的方式来动态访问一个对象属性,其中 expression 表达式会先被执行,然后使用返回值来访问属性。

在 TypeScript 中也可以通过类似的方式,只不过这里的 expression 要换成类型:

interface Foo {
    propA: number;
    propB: boolean;
}

type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean
1
2
3
4
5
6
7

要注意其访问方式与返回值均是类型。上面代码中的 'propA''propB' 都是字符串字面量类型,而不是一个 JavaScript 字符串值。

索引类型查询的本质其实就是,通过键的字面量类型('propA')访问这个键对应的键值类型(number)。

# 映射类型

映射类型的主要作用是基于键名,映射到键值类型。概念不好理解,直接看例子:

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

这个工具类型接受一个对象类型,使用 keyof 获得这个对象类型的键名组成字面量联合类型,然后通过映射类型(即这里的 in 关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string

放到代码中的用法:

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 上述代码就等价于
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

要说这个用法的实际场景的话,可能是这样:

type Clone<T> = {
  [K in keyof T]: T[K];
};
1
2
3

就是先拿到键,然后通过索引类型访问(T[K])获得值的类型,最终实现了接口的克隆。这样就结合到了「索引类型」这一主题下的几个功能了:

  • K in 是上面提到的映射类型的语法
  • [K in keyof T][] 是索引签名类型的语法
  • keyof T 是索引类型查询的语法(keyof 操作符)
  • T[K] 是索引类型访问的语法

# 总结

类型别名、联合类型、索引类型以及映射类型,这些主要都用于创建新类型,它们的实现方式与常见搭配总结如下:

  类型别名  将一组类型/类型结构封装,作为一个新的类型  联合类型、映射类型  工具类型  在类型别名的基础上,基于泛型去动态创建新类型  基本所有类型工具  联合类型  创建一组类型集合,满足其中一个类型即满足这个联合类型(||)  类型别名、工具类型  交叉类型  创建一组类型集合,满足其中所有类型才满足映射联合类型(&&)  类型别名、工具类型  索引签名类型  声明一个拥有任意属性,键值类型一致的接口结构  映射类型  索引类型查询  从一个接口结构,创建一个由其键名字符串字面量组成的联合类型  映射类型  索引类型访问  从一个接口结构,使用键名字符串字面量访问到对应的键值类型  类型别名、映射类型  映射类型  从一个联合类型依次映射到其内部的每一个类型  工具类型  类型工具  创建新类型的方式  常见搭配

(完)