前两章我们知道 TypeScript 给 JavaScript 加了一套静态类型系统,还支持了泛型和各种类型运算逻辑。那么这个类型系统里都有哪些类型?支持哪些类型运算逻辑?

TypeScript 类型系统中的类型

静态类型系统的目的是把类型检查从运行时提前到编译时,那 TS 类型系统中肯定要把 JS 的运行时类型拿过来,也就是 number、boolean、string、object、bigint、symbol、undefined、null 这些类型,还有就是它们的包装类型 Number、Boolean、String、Object、Symbol。

这些很容易理解,给 JS 添加静态类型,总没有必要重新造一套基础类型吧,直接复用 JS 的基础类型就行。

复合类型方面,JS 有 class、Array,这些 TypeScript 类型系统也都支持,但是又多加了三种类型:元组(Tuple)、接口(Interface)、枚举(Enum)。

元组

元组(Tuple)就是元素个数和类型固定的数组类型:

1
type Tuple = [number, string];

接口

接口(Interface)可以描述函数、对象、构造器的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IPerson {
name: string;
age: number;
}

class Person implements IPerson {
name: string;
age: number;
}

const obj: IPerson = {
name: 'guang',
age: 18
}
1
2
3
4
5
6
7
interface SayHello {
(name: string): string;
}

const func: SayHello = (name: string) => {
return 'hello,' + name
}
1
2
3
4
5
6
7
interface PersonConstructor {
new (name: string, age: number): IPerson;
}

function createPerson(ctor: PersonConstructor): IPerson {
return new ctor("guang", 18);
}

对象类型、class 类型在 TypeScript 里也叫做索引类型,也就是索引了多个元素的类型的意思。对象可以动态添加属性,如果不知道会有什么属性,可以用可索引签名

1
2
3
4
5
6
interface IPerson {
[prop: string]: string | number;
}
const obj:IPerson = {};
obj.name = 'guang';
obj.age = 18;

枚举

枚举(Enum)是一系列值的复合:

1
2
3
4
5
6
7
8
9
enum Transpiler {
Babel = 'babel',
Postcss = 'postcss',
Terser = 'terser',
Prettier = 'prettier',
TypeScriptCompiler = 'tsc'
}

const transpiler = Transpiler.TypeScriptCompiler;

TypeScript 还支持字面量类型,也就是类似 1111、’aaaa’、{ a: 1} 这种值也可以做为类型。

其中,字符串的字面量类型有两种

  • 一种是普通的字符串字面量,比如 ‘aaa’
  • 另一种是模版字面量,比如 aaa${string},它的意思是以 aaa 开头,后面是任意 string 的字符串字面量类型

想要约束以某个字符串开头的字符串字面量类型时可以这样写:

1
2
3
4
5
6
7
function demo(str: `#${string}`) {}

//报错:类型“"aaa"”的参数不能赋给类型“`#${string}`”的参数。
demo("aaa");


demo("#aaa");

还有四种特殊的类型:void、never、any、unknown:

  • never 代表不可达,比如函数抛异常的时候,返回值就是 never。
  • void 代表空,可以是 undefined 或 never。
  • any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never)。
  • unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型。

这些就是 TypeScript 类型系统中的全部类型了,大部分是从 JS 中迁移过来的,比如基础类型、Array、class 等,也添加了一些类型,比如 枚举(enum)、接口(interface)、元组等,还支持了字面量类型和 void、never、any、unknown 的特殊类型。

类型装饰

除了描述类型的结构外,TypeScript 的类型系统还支持描述类型的属性,比如是否可选,是否只读等:

1
2
3
4
5
6
interface IPerson {
readonly name: string;
age?: number;
}

type tuple = [string, number?];

TypeScript 类型系统中的类型运算

条件

TypeScript 里的条件判断是 extends ? :,叫做条件类型(Conditional Type)比如

1
2
//false
type res = 1 extends 2 ? true : false;

这就是 TypeScript 类型系统里的 if else。

上面这样的逻辑没啥意义,静态的值自己就能算出结果来,为什么要用代码去判断呢?

所以,类型运算逻辑都是用来做一些动态的类型的运算的,也就是对类型参数的运算。

1
2
3
4
5
6
type isTwo<T> = T extends 2 ? true : false;

//false
type res1 = isTwo<1>;
//true
type res2 = isTwo<2>;

这种类型也叫做高级类型

高级类型的特点是传入类型参数,经过一系列类型运算逻辑后,返回新的类型。

推导 infer

如何提取类型的一部分呢?答案是 infer。

比如提取元组类型的第一个元素:

1
2
3
type first<U extends unknown[]> = U extends [infer R, ...infer _] ? R : never;
// 1
type res3 = first<[1, 23, 4]>;

第一个 extends 不是条件,条件类型是 extends ? :,这里的 extends 是约束的意思,也就是约束类型参数只能是数组类型。

因为不知道数组元素的具体类型,所以用 unknown。

联合

代表类型可以是几个类型之一。

1
2
3
4
5
6
7
8
9
interface obj {
value: string | number;
}
const person: obj = {
value: 1,
};
const person2: obj = {
value: "aaa",
};

交叉

代表对类型做合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface obj1 {
name: string;
}
interface obj2 {
age: number;
}

type obj3 = obj1 & obj2;

const person3: obj3 = {
name: "bbb",
age: 2,
};

同一类型可以合并,不同的类型没法合并,会被舍弃:

1
2
// never
type res4 = string & number;

映射

对象、class 在 TypeScript 对应的类型是索引类型(Index Type),那么如何对索引类型作修改呢?

1
2
3
type MapType<T> = {
[Key in keyof T]?: T[Key]
}

keyof T 是查询索引类型中所有的索引,叫做索引查询

T[Key] 是取索引类型某个索引的值,叫做索引访问

in 是用于遍历联合类型的运算符。

1
2
3
4
5
6
7
8
9
type MapType<T> = {
[Key in keyof T]: [T[Key], T[Key], T[Key]];
};

// "a" | "b"
type keyofRes = keyof { a: 1; b: 2 };

// { a: [1, 1, 1], b: [2, 2, 2];}
type res5 = MapType<{ a: 1; b: 2 }>;

映射类型就相当于把一个集合映射到另一个集合,这是它名字的由来

除了值可以变化,索引也可以做变化,用 as 运算符,叫做重映射

1
2
3
4
5
6
7
8
9
type MapType1<T> = {
[Key in keyof T as `${Key & string}${Key & string}${Key & string}`]: [
T[Key],
T[Key],
T[Key]
];
};
// { aaa: [1, 1, 1], bbb: [2, 2, 2];}
type res6 = MapType1<{ a: 1; b: 2 }>;

索引类型(对象、class 等)可以用 string、number 和 symbol 作为 key

这里 keyof T 取出的索引就是 string | number | symbol 的联合类型,和 string 取交叉部分就只剩下 string 了。

就像前面所说,交叉类型会把同一类型做合并,不同类型舍弃。

总结

  • 给 JavaScript 添加静态类型系统,那肯定是能复用的就复用
  • 额外加了接口(interface)、枚举(enum)、元组这三种复合类型(对象类型、class 类型在 TypeScript 里叫做索引类型)
  • 还有 void、never、any、unkown 四种特殊类型,以及支持字面量做为类型。
  • 此外,TypeScript 类型系统也支持通过 readonly、?等修饰符对属性的特性做进一步的描述。
  • TypeScript 支持对类型做运算,这是它的类型系统的强大之处,也是复杂之处。
  • TypeScript 支持条件、推导、联合、交叉等运算逻辑,还有对联合类型做映射。
  • 这些逻辑是针对类型参数,也就是泛型(类型参数)来说的,传入类型参数,经过一系列类型运算逻辑后,返回新的类型的类型就叫做高级类型,如果是静态的值,直接算出结果即可,没必要写类型逻辑。