类型细化
有的时候我们会对某个值做类型判断,而 TS 能从中推断该值更具体的类型:
function padLeft(padding: number | string, input: string) {
if (typeof padding === 'number') {
// 这里 TS 会推断出 `padding` 是 `number` 类型
return ' '.repeat(padding) + input;
}
// 这里 TS 会推断出 `padding` 是 `string` 类型
return padding + input;
}
这种类型推断的特性称为“类型细化”( Narrowing ),一般存在于各种控制语句上,比如 if-else
、三元条件语句、循环、真值检查等。有许多种情况可以应用这一特性。
typeof
类型保护
typeof
是 JS 里的操作符,能够在运行时里提供值类型信息。TS 能够分析的 typeof
的返回值有:
- "string"
- "number"
- "bigint"
- "boolean"
- "undefined"
- "object"
- "function"
TS 有着自己的 typeof
处理,因为 JavaScript 中可能会存在一些奇怪的行为,比如 null
会被认为是 object
类型(实际上 null
自身是一种类型):
function printAll(strs: string | string[] | null) {
if (typeof strs === 'object') {
// ERROR: 这里的 `strs` 也有可能是 `null`
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}
像 typeof 这样类型检查的语句在 TS 中称为类型保护( Type guard )。
真值检查
在 JS 里我们可以在条件语句、&&
、||
、if
语句或否定符 !
等里使用任何表达式。比如我们可以在 if
里放非 boolean
类型的条件语句。JS 会将条件语句强制转为布尔值,比如这些值会被转为 false
:
0
NaN
"" ,即空字符串
0n ,即
bigint
中的 0null
undefined
你也可以用 Boolean
函数将值转为布尔值,或者用双否定 !!
。而后者其实会推断为一个布尔值字面量类型(比如例子中为 true
),前者仅仅是个 boolean
类型:
Boolean('hello'); // 类型为 `boolean` ,值为 `true`
!!'world'; // 类型和值均为 `true`
我们可以利用这一行为来进行空值检查:
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === 'object') {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}
但是 不要 写成如下的例子,因为空字符串是 string
类型,但是会转为布尔值 false
:
function printAll(strs: string | string[] | null) {
if (strs) {
if (typeof strs === 'object') {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}
}
判等语句
TS 会对 switch
语句,以及 ===
、!==
、==
、!=
等判等操作中来细化类型:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// 这里 `x` 和 `y` 类型均被细化为 `string`
x.toUpperCase();
y.toLowerCase();
} else {
// 这里 `x` 类型被细化为 `string | number`
console.log(x);
}
}
对于 ==
和 !=
这类弱化的判等,TS 也会有正确的类型细化:
function anotherExample(x: number | null | undefined) {
if (x != null) {
// 这里 `x` 类型被细化为 `number`
console.log(x);
}
}
in
操作符
TS 会根据 in
操作语句来细化类型:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
return animal.swim();
}
return animal.fly();
}
instanceof
操作符
JS 中 instanceof
用于判断是否在某个类的原型链上。TS 也会据此细化类型:
function logValue(x: Date | string) {
if (x instanceof Date) {
// 这里 `x` 类型被细化为 `Date`
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
赋值语句
在赋值的时候,TS 也会细化类型,不需要我们自己判断:
// 只有声明语句能确定变量类型
let x = Math.random() < 0.5 ? 10 : 'hello world!';
x = 1; // `x` 类型被细化为 `number`
x = 'goodbye!'; // `x` 类型被细化为 `string`
x = false; // ERROR, 不能将 `boolean` 赋值给 `string | number` 类型
类型细化不是类型改变。
类型断言
函数的返回值可以是个类型断言,这将告诉 TS 是否需要类型细化:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
// TS 会根据类型断言对 pet 进行类型细化
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
如果没有 pet is Fish
的类型断言,那么条件语句里的 pet
将不会做类型细化。
可区分的联合类型
在下面这个例子中,Shape
是一个联合类型:
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
sideLength: number;
}
type Shape = Circle | Square;
但在处理 Shape
对象时,TS 可以根据 kind
属性来细化类型:
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
// 这里 `shape` 类型被细化为了 `Circle` ,下同
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
}
}
never
类型
在细化过程中,如果遇到了不可能存在的情况,TS 会用 never
类型代替:
function print(x: number | string) {
if (typeof x === 'number') {
// ...
} else if (typeof x === 'string') {
// ...
} else {
// 这是一个不可能达到的代码块,所以 TS 会将 x 类型标为 `never`
}
}