TypeScript 的类型系统是一种静态契约机制,它定义了值在编译期必须满足的结构和行为约束。类型可以描述对象的成员(名称、类型、可选性、可写性)、函数签名、字面量取值范围等。联合类型 A | B 表示一个值可能是 A 或 B,从集合论角度看是类型集合的并集,但它不会把成员结构合并,而是保持"二选一"的语义。所以当我们将一个变量声明为联合类型的时候,在默认的情况下只有针对交集 数据成员的访问是安全的。为了不仅限于访问联合类型的交集成员,但是并集成员范围又太宽,在编程的时候我们需要引入响应的条件判断,缩小成员的范围,使针对某些API的访问控制在一个安全的边界之内,我们将其称为类型收窄(Narrowing)。
1. typeof操作符
JavaScript 支持 typeof 运算符,它可以提供运行时值类型的基本信息。TypeScript 期望它返回一组特定的字符串:
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
注 : typeof null === "object" 是成立的,所以下面代码中的strs可能为null,具体类型为string[]|null:
typescript
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
//'strs' is possibly 'null'. strs的类型为 string[]
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
2. 基于真值类型的条件判断
在 JavaScript 中,我们可以在条件语句、&&、||、if 语句、布尔否定(!)等中使用任何表达式 ,并不要求其条件必须是布尔类型。在 JavaScript 中,类似 if 这样的语句首先会将条件强制为布尔值以使其有意义,然后根据结果是真还是假来选择执行分支。
- 0
- NaN
- "" (the empty string)
- 0n (the bigint version of zero)
- null
- undefined
所以上面的问题可以通过 && 操作符来确保strs不可能为null。
typescript
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
3. 对等判断
TypeScript 还使用 switch 语句和相等性检查(例如 ===、!==、== 和 !=)来缩小类型范围。
typescript
function example(x: string | number, y: string | boolean) {
if (x === y) { // 既然对等,必然是字符串
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x); //(parameter) x: string | number
console.log(y); //(parameter) y: string | boolean
}
}
注 : == /!= 和===/!==的区别:
typescript
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// 排除null和undefined.
if (container.value != null) {
console.log(container.value);
// (property) Container.value: number
container.value *= factor;
}
}
4. in 操作符
运算符 in用于判断一个对象或其原型链 是否具有名为 name 的属性。TypeScript 会利用这一点来缩小潜在类型的范围:
typescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; //(parameter) animal: Fish | Human
} else {
animal; //(parameter) animal: Bird | Human
}
}
注 :对于可缺省数据成员,可能存在,也可能不存在。
5. instanceof 操作符
运算符 instanceof 可以检查一个值是否是另一个值的实例 。更具体地说,在 JavaScript 中,x instanceof Foo 检查 x 的原型链 是否包含 Foo.prototype。
typescript
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // (parameter) x: Date
} else {
console.log(x.toUpperCase()); // (parameter) x: string
}
}
6. 赋值语句
当我们给任何变量赋值时,TypeScript 会查看赋值语句的右侧,并相应地缩小左侧的范围。如下的代码是合法的:
typescript
let x = Math.random() < 0.5 ? 10 : "hello world!"; //let x: string | number
x = 1;
console.log(x); //let x: number
x = "goodbye!";
console.log(x); //let x: string
如下则是有问题的:
typescript
let x = Math.random() < 0.5 ? 10 : "hello world!"; //let x: string | number
x = 1;
console.log(x); //let x: number
x = true; // 类型不匹配,赋值失败.
console.log(x); // let x: string | number
7. 控制流分析
TypeScript会自动分析控制流,并在相应分支智能实施类型收窄。
typescript
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input; // 排除number,只能是string
}
8. 类型断言
要定义用户自定义type guards,我们只需要定义一个返回类型为类型谓词的函数:
typescript
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
注 :TypeScript函数的两种返回形式:基于确定类型的值 + 类型谓词
9. assert 函数
assert 函数可以在调用之后根据指定的断言强行实施类型收窄。
typescript
function multiply(x, y) {
assert(typeof x === "number");
assert(typeof y === "number");
return x * y;
}
10. 可区分的联合(Discriminated Unions)
在 TypeScript 中,可区分联合类型(Discriminated Unions)是一种通过共享的判别字段 来安全区分不同对象类型的模式。它能让编译器在 switch 或 if 判断时自动缩小类型范围,并提供 穷尽性检查,避免遗漏分支。
它用来解决这种问题:
typescript
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
function getArea(shape: Shape) {
return shape.kind == "circle"
? Math.PI * shape.radius ** 2 //'shape.radius' 可能是'undefined'.
: shape.sideLength ** 2 ; //'shape.sideLength' 可能是'undefined'.
}
解决方案:
typescript
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
return shape.kind == "circle"
? Math.PI * shape.radius ** 2
: shape.sideLength ** 2 ;
}
11. 穷尽性检查(Exhaustiveness checking)
缩小范围时,你可以将联合类型的选项减少到排除所有可能性的程度 。在这种情况下,TypeScript 会使用 never 类型来表示不应该存在的状态。never 类型可以赋值给任何类型;然而,除了 never 本身之外,没有任何类型可以赋值给 never。这意味着你可以使用类型缩小,并依靠 never 的出现来进行 switch 语句中的穷举检查。
typescript
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}