一.类型别名
type PersonName = string;
type PhoneNumber = string;
type PhoneBookItem = [PersonName, PhoneNumber];
type PhoneBook = PhoneBookItem[];
let book: PhoneBook = [
['Lily', '1234'],
['Jean', '1234']
];
type
关键字能为现有类型创建一个别名,从而增强其可读性
接口与类型别名
类型形式上与接口有些类似,都支持类型参数,且可以引用自身,例如:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
interface ITree<T> {
value: T;
left: ITree<T>;
right: ITree<T>;
}
但存在一些本质差异:
类型别名并不会创建新类型,而接口会定义一个新类型
允许给任意类型起别名,但无法给任意类型定义与之等价的接口(比如基础类型)
无法继承或实现类型别名(也不能扩展或实现其它类型),但接口可以
类型别名能将多个类型组合成一个具名类型,而接口无法描述这种组合(交叉、联合等)
// 类型组合,接口无法表达这种类型
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
function findSomeone(people: LinkedList<Person>, name: string) {
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;
}
应用场景上,二者区别如下:
接口:OOP场景(因为能被继承和实现,维持着类型层级关系)
类型别名:追求可读性的场景、接口无法描述的场景(基础类型、交叉类型、联合类型等)
二.字面量类型
存在两种字面量类型:字符串字面量类型与数值字面量类型
字符串
字符串字面量也具有类型含义,例如:
let x: 'string';
// 错误 Type '"a"' is not assignable to type '"string"'.
x = 'a';
// 正确
x = 'string';
可以用来模拟枚举的效果:
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === 'ease-in') {}
else if (easing === 'ease-out') {}
else {
// 自动缩窄到"ease-in-out"类型
}
}
}
// 错误 Argument of type '"linear"' is not assignable to parameter of type 'Easing'.
new UIElement().animate(0, 0, 'linear');
不同的字符串字面量属于不同的具体类型,因此,(如果必要的话)可以这样重载:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element {
return document.createElement(tagName);
}
数值
数值字面量同样具有类型含义:
// 返回骰子的6个点数
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
看起来只是个匿名数值枚举,似乎没什么存在必要
存在意义
实际上,字面量类型的意义在于编译时能够结合类型信息“推理”,例如:
function foo(x: number) {
// 错误 This condition will always return 'true' since the types '1' and '2' have no overlap.
if (x !== 1 || x !== 2) { }
}
function bar(x: string) {
// 错误 This condition will always return 'false' since the types '"1"' and '"2"' have no overlap.
if (x === '1' && x === '2') {
//...
}
}
这种类型完整性补充让TypeScript能够更细致地“理解”(静态分析)代码含义,进而发现一些不那么直接的潜在问题
Nevertheless, by pairing a type with it’s unique inhabitant, singleton types bridge the gap between types and values.
三.枚举与字面量类型
我们知道有一种特殊的枚举叫联合枚举,其成员也具有类型含义,例如:
// 联合枚举
enum E {
Foo,
Bar,
}
// 枚举的类型含义
function f(x: E) {
// 错误 This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
if (x !== E.Foo || x !== E.Bar) {
//...
}
}
这与字面量类型中的例子非常相似:
function f(x: 'Foo' | 'Bar') {
// 错误 This condition will always return 'true' since the types '"Foo"' and '"Bar"' have no overlap.
if (x !== 'Foo' || x !== 'Bar') {
//...
}
}
P.S.类比起见,这里用字符串字面量联合类型('Foo' | 'Bar'
)模拟枚举E
,实际上枚举E
等价于数值字面量联合类型(0 | 1
),具体见二.数值枚举
从类型角度来看,联合枚举就是由数值/字符串字面量构成的枚举,因此其成员也具有类型含义。名称上也表达了这种联系:联合枚举,即数值/字符串联合
P.S.枚举成员类型与数值/字符串字面量类型也叫单例类型(singleton types):
Singleton types, types which have a unique inhabitant.
也就是说,一个单例类型下只有一个值,例如字符串字面量类型'Foo'
只能取值字符串'Foo'
四.可区分联合
结合单例类型、联合类型、类型保护和类型别名可以建立一种模式,称为可区分联合(discriminated unions)
P.S.可区分联合也叫标签联合(tagged unions)或代数数据类型(algebraic data types),即可运算、可进行逻辑推理的类型
具体地,可区分联合一般包括3部分:
- 一些具有公共单例类型属性的类型——公共单例属性即可区分的特征(或者叫标签)
- 一个指向这些类型构成的联合的类型别名——即联合
- 针对公共属性的类型保护
通过区分公共单例属性的类型来缩窄父类型,例如:
// 1.一些具有公共单例属性(kind)的类型
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
// 2.定义联合类型,并起个别名
type Shape = Square | Circle;
// 3.具体使用(类型保护)
function area(s: Shape) {
switch (s.kind) {
// 自动缩窄到Square
case "square": return s.size * s.size;
// 自动缩窄到Circle
case "circle": return Math.PI * s.radius ** 2;
}
}
算是对instanceof类型保护的一种补充,都用于检测复杂类型的兼容关系,区别如下:
instanceof类型保护:适用于有明确继承关系的父子类型
可区分联合类型保护:适用于没有明确继承关系(运行时通过
instanceof
检测不出继承关系)的父子类型
完整性检查
有些时候可能想要完整覆盖联合类型的所有组成类型,例如:
type Shape = Square | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
// 潜在问题:漏掉了"circle"
}
}
可以通过never
类型来实现这种保障(Never类型为数不多的应用场景之一):
function assertNever(x: never) {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
// case "circle": return s.radius * s.radius;
// 错误 Argument of type 'Circle' is not assignable to parameter of type 'never'.
default: return assertNever(s);
}
}
如果没有完整覆盖,就会走到default
分支把s: Shape
传递给x: never
引发类型错误(完整覆盖了的话,default
就是不可达分支,不会引发never
错误)。能够满足完整性覆盖要求,但需要额外定义一个assertNever
函数
P.S.关于Never类型的更多信息,见基本类型_TypeScript笔记2
此外,还有一种不那么准确,但也有助于检查完整性的方法:开启--strictNullChecks
选项,并标明函数返回值。利用默认返回undefined
来保证完整性,例如:
// 错误 Function lacks ending return statement and return type does not include 'undefined'.
function area(s: Shape): number {
switch (s.kind) {
case "square": return s.size * s.size;
}
}
实质上是非空返回值检测,不像assertNever
是精确到switch
粒度的,相对脆弱(有默认返回值,或有多个switch
都会破坏完整性检查)