第二章:TypeScript 的类型系统
第6条 使用你的编辑器来询问和浏览类型系统
第7条 将类型看作是值的集合
基本类型
let x: never; // never 类型,对应空集 {}
let x: never = 12; // 报错,因为空集不包含 12
type A = 'A'; // 单值集合 {'A'}
type B = 'B'; // 单值集合 {'B'}
type Twelve = 12;; // 单值集合 {12}
type AB = 'A' | 'B'; // 集合 {'A','B'},也可以看成是 {'A'} 和 {'B'} 的并集
type A_B = 'A' & 'B'; // never 类型,对应空集 {},也可以看成是 {'A'} 和 {'B'} 的交集
type AB12 = 'A' | 'B' | 12; // 集合 {'A','B',12}
const a: AB = 'A'; // {'A'} 是 {'A','B'} 的子集
const c: AB = 'C'; // 报错,因为 {'C'} 不是 {'A','B'} 的子集
const ab: AB = Math.random() < 0.5 ? 'A' : 'B'; // {'A','B'} 是 {'A','B'} 的子集
type Int = 1 | 2 | 3 | 4 | 5 ... // 整型相当于无穷多整数的集合
interface
和基本类型相比,interface
稍微不同。
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }
如果不使用 extends
,上面的类型定义可以写成如下形式:
interface Vector1D { x: number; }
interface Vector2D { x: number; y: number; }
interface Vector3D { x: number; y: number; z: number; }
如果把 interface
看成是集合,那么 Vector3D
是 Vector2D
的子集,Vector2D
是 Vector1D
的子集,可以表示为:
属性越多能表示的集合越小,属性越少能概括的集合越大。两个 interface
的交集包含它们两者所有的属性,并集拥有它们共同的属性,如果没有共同属性,那么并集为空集。例如以下代码:
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
};
type K = keyof Person | Lifespan; // never
PersonSpan
是 Person
和 Lifespan
的交集,也是子集。另外也可以用 extends
表示:
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
我们还可以借助 keyof
进一步加深对 interface
的理解。
两个 interface
没有共同属性:
interface A {
value: string;
}
interface B {
message: string;
}
type C = keyof (A & B); // 'value' | 'message'
type D = keyof (A | B); // never
type E = (keyof A) | (keyof B); // 'value' | 'message'
type F = (keyof A) & (keyof B); // never
两个 interface
有共同属性:
interface A {
code: number;
value: string;
}
interface B {
code: number;
message: string;
}
type C = keyof (A & B); // 'code' | 'value' | 'message'
type D = keyof (A | B); // 'code'
type E = (keyof A) | (keyof B); // 'code' | 'value' | 'message'
type F = (keyof A) & (keyof B); // 'code'
我们可以发现存在这样的交换律:
keyof (A & B) = (keyof A) | (keyof B)
keyof (A | B) = (keyof A) & (keyof B)
extends
extends
除了用于对象继承,还可以用来约束泛型类型,例如以下代码:
function getKey<K extends string>(val: any, key: K) {
// ...
}
getKey({}, 'x'); // OK, 'x' extends string
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); // OK, 'a'|'b' extends string
getKey({}, document.title); // OK, string extends string
getKey({}, 12);
// Argument of type 'number' is not assignable to parameter
// of type 'string'.
其中 K
是 string
的子集。
interface Point {
x: number;
y: number;
}
type PointKeys = keyof Point; // Type is "x" | "y"
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
// ...
}
const pts: Point[] = [{ x: 1, y: 1 }, { x: 2, y: 0 }];
sortBy(pts, 'x'); // OK, 'x' extends 'x'|'y' (aka keyof T)
sortBy(pts, 'y'); // OK, 'y' extends 'x'|'y'
sortBy(pts, Math.random() < 0.5 ? 'x' : 'y'); // OK, 'x'|'y' extends 'x'|'y'
sortBy(pts, 'z');
// Argument of type '"z"' is not assignable to parameter
// of type 'keyof Point'.
其中 K
是 Point
所有 key
的子集。
数组(Array)和元组(Tuple)
const list = [1, 2]; // Type is number[]
const tuple: [number, number] = list;
// Type 'number[]' is not assignable to type '[number, number]'.
// Target requires 2 element(s) but source may have fewer.
const triple: [number, number, number] = [1, 2, 3];
const double: [number, number] = triple;
// Type '[number, number, number]' is not assignable to type '[number, number]'.
// Source has 3 element(s) but target allows only 2.
Exclude
可以使用 Exclude
排除一些类型:
type T = Exclude<string | Date, string | number>; // Type is Date
type NonZeroNums = Exclude<number, 0>; // Type is still just number
TypeScript 和集合理论术语对照
TypeScript 术语 | 集合术语 |
---|---|
never |
∅ (空集) |
字面量类型 | 单元素集合 |
Value 可赋值给 T | Value ∈ T (属于) |
T1 可赋值给 T2 | T1 ⊆ T2 (子集) |
T1 extends T2 |
T1 ⊆ T2 (子集) |
T1 | T2 |
T1 ∪ T2 (并集) |
T1 & T2 |
T1 ∩ T2 (交集) |
unknown |
全集 |
第8条 区分类型空间还是值空间
TypeScript 中的符号存在两种空间:
- 类型空间(Type Space)
- 值空间(Value Space)
不同关键词定义的符号可能属于不同空间,最常见的比如:
type
、interface
引入类型const
、let
引入值class
、enum
引入类型和值
可以用下面的代码来检验我们对类型和值的判断:
class Cylinder {
radius = 1;
height = 1;
}
const c = new Cylinder();
type U = typeof c; // Type is Cylinder
const v = typeof Cylinder; // Value is "function"
type T = typeof Cylinder; // Type is typeof Cylinder
type C = InstanceType<typeof Cylinder>; // Type is Cylinder
第9条 倾向于类型声明而不是类型断言
interface Person {
name: string
};
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}'
// but required in type 'Person'
const bob = {} as Person;
const rose: Person = {
name: 'Alice',
occupation: 'TypeScript developer'
// ~~~~~~~~~ Object literal may only specify known properties
// and 'occupation' does not exist in type 'Person'
};
const jack = {
name: 'Bob',
occupation: 'JavaScript developer'
} as Person;
相比类型断言,类型声明可以获得更好类型检查。
第10条 避免使用对象包装类型(String, Number, Boolean, Symbol, BigInt)
以字符串为例:
'primitive'.charAt(3); // "m"
作为基本类型,字符串字面量本身不包含方法,在调用时 JavaScript 会自动将它包装成 String
对象,调用完后重新返回基本类型,可以通过如下方式验证:
const originalCharAt = String.prototype.charAt;
String.prototype.charAt = function (pos) {
console.log(this, typeof this, pos);
return originalCharAt.call(this, pos);
};
console.log('primitive'.charAt(3));
执行结果:
String {"primitive"} "object" 3
m
字符串基本类型和字符串对象并不相等:
"hello" === "hello"; // true
"hello" === new String("hello"); // false
new String("hello") === new String("hello"); // false
字符串基本类型的隐式包装类型转换,可以解释以下古怪现象:
> var x = "hello"
> x.language = 'English'
'English'
> x.language
undefined
在 TypeScript 中,可以将字符串基本类型 string
赋值给包装类型 String
,但无法反过来将 String
赋值给 string
。
function getStringLen(foo: String) {
return foo.length;
}
getStringLen("hello"); // OK
getStringLen(new String("hello")); // OK
function isGreeting(phrase: String) {
return [
'hello',
'good day'
].includes(phrase);
// ~~~~~~
// Argument of type 'String' is not assignable to parameter
// of type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object;
// prefer using 'string' when possible
}