Type Narrowing
类型守卫是一种缩小变量范围的方式,当需要根据不同的类型进行不同的操作时,类型守卫将会非常有用。考虑如下的例子:
function padLeft(padding: string | number, input: string): string {
// Argument of type 'string | number' is not assignable to parameter of type 'number'.
return ' '.repeat(padding) + input;
}
这里会产生一个错误,由于Arrar.prototype.repeat()只能接受number类型的参数,而我们传递了string | number类型。换句话说,我们没有明确检查padding是否是一个number,也没有处理它是string的情况:
function padLeft(padding: string | number, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
TypeSctipt中将类型转换为比声明更加具体的类型的过程被称为缩小范围,用于检测值类型的过程称为类型守卫。在TypeScript中缩小范围有几种不同构造。
typeof类型守卫
在JavaSctipt中,提供了typeof操作符,可以在运行时提供值类型的基本信息。在TypeScript中,typeof行为与JavaScript一致,总是返回如下结果之一:
| number | string | boolean | bigint | symbol | undefined | function | object |
|---|---|---|---|---|---|---|---|
| 数值型 | 字符串型 | 布尔型 | 大整型 | 符号型 | 未定义 | 函数型 | 对象型 |
在 TypeScript 中,针对typeof返回值的检查是类型守卫。因为TypeScript对不同值的typeof操作进行了编码。如下示例中,typeof没有返回字符串的null :
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
// 'strs' is possibly 'null'.
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
上述示例中,由于typeof null === 'object',而TypeScript让我们知道strs被缩小为string[] | null而不是仅仅是string[] ,从而在一定程度上减少了运行时的错误。
真值缩小
在JavaScript中,条件语句中可以使用任何表达式,&&,||,if语句,!布尔否定等。if语句并不总是希望条件是boolean类型:
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
在JavaScript中,条件语句(比如if语句)需要将它们的条件强制转换为布尔值来理解它们,然后根据结果是true还是false来选择不同的分支。0、NaN、''、0n、null、undefined、false 都被强制转为false,其他的都强转为true。你可以可以通过Boolean函数或者双重布尔否定,TypeScript两种转换方式的区别在于:双重布尔否定可以推断出一个狭窄的字面布尔类型true ,而Boolean构造函数转换后推断结果为boolean。
// const byBoolean: boolean
const byBoolean = Boolean('hello')
// const byDoubleExclamation: true
const byDoubleExclamation = !!'hello'
但请记住,在原始类型上进行真值检查通常容易出错:
function printAll(strs: string | string[] | null) {
if (strs) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
如上示例,程序可能不能正确地处理空字符串情况。
TypeScript 往往可以帮助您早期发现 bug,但如果您选择不对一个值做任何处理,那么TypeScript可以做的事情也很有限,而过度规定也并不是好的解决办法。如果您愿意的话,可以通过使用lint工具来确保您处理类似这样的情况。
还有一种通过真值缩小范围的问题:在Boolean取反时,有一个筛选出取反分支的过程:
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
相等性缩小
TypeSctipt也会使用switch语句和相等性来缩小类型:
function equality(x: number | string, y: boolean | string) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
y.toLowerCase();
} else {
// other branch
}
}
如上,由于x和y的公共类型是string,因此TypeScript知道如果要相等,那么两者必须都是string。
同样的,与具体字面量值进行检查(而不是变量)同样也可行:
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
使用 ==和!=的松散相等性检查也能被正确地缩小范围:
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
container.value *= factor;
}
}
multiplyValue({ value: null }, 2)
in操作符缩小
JavaScript中,in操作符用来检测某个对象或其原型链上是否存在某个属性,TypeScript也将这视作一种缩小可能类型范围的方式。
例如,对于value in x,如果value是字符串字面量,x是联合类型,true分支类型缩小要么是可选属性要么是必选属性,而false分支类型缩小为可选属性或者无此属性。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
在类型缩小时,可选属性在两个分支上都存在:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void, fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ('swim' in animal) {
// (parameter) animal: Fish | Human
console.log(animal);
} else {
// (parameter) animal: Bird | Human
console.log(animal);
}
}
instanceof缩小
在JavaScript中,instanceof运算符检查一个值是否为某个构造器的“实例”。
而在TypeScript中,instanceof也是一种类型守卫,可使用此运算符来缩小代码分支。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.getTime());
} else {
console.log(x.toUpperCase());
}
}
logValue(new Date())
赋值缩小
当对任意变量进行赋值时,TypeScript会根据赋值的右侧来相应地缩小左侧的类型:
let sx = Math.random() < 0.5 ? 10 : "hello world!";
sx = 1 // 将类型缩小为number
// let sx: number
console.log(sx);
sx = 'abcd'
// let sx: string
console.log(sx);
控制流程分析
基于可达性的代码分析被称作控制流分析,当TypeScript遇到类型保护和赋值时,会利用这种流程分析缩小类型。在分析变量时,控制流可能会不断地分裂和重新合并,因此在每个点观察到的该变量的类型可能会不同。
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
// boolean
console.log(x);
if (Math.random() < 0.5) {
x = "hello";
// string
console.log(x);
return x;
} else {
x = 100;
// number
console.log(x);
return x;
}
}
类型谓语
以上几种方式主要利用现有的JavaScript提供的特性来处理缩小类型,然而有时你可能需要更直接地控制整个代码中类型的变化。考虑如下示例:
function isString(str: unknown) {
return typeof str === 'string';
}
function isNumber(num: unknown) {
return typeof num === 'number';
}
function formatArray(str: unknown): string[] {
// 无法分析推断出其类型
if (isString(str)) {
// str' is of type 'unknown'
return str.split('')
}
if (isNumber(str)) {
return str.toString().split('')
}
return []
}
如上,TypeScript无法间接地分析推断出isString的类型,只能根据参数类型来推断,导致类型检查不通过,此时可通过类型谓词来解决。
要定义一个用户自定义类型保护,需要定义一个其返回类型为类型谓词的函数,通常返回一个boolean类型,被用来缩小变量的类型。
类型谓词的形式为parameterName is Type,其中parameterName必须是当前函数签名中的参数名。
function isString(value: unknown): value is string {
return typeof value === "string";
}
如上,value is string是一个类型谓词,表示如果return后面表达式是真,就判定value是string类型。
当类型谓词函数被某个变量调用时,TypeScript将会将该变量缩小为特定的类型:
function formatArray(str: unknown): string[] {
// 类型检测通过
if (isString(str)) {
return str.split('')
}
if (isNumber(str)) {
return str.toString().split('')
}
return []
}
类型谓词本质是赋予了函数typeof的功能。
判别式类型
当联合类型的每个成员都包含具有字面量类型的公共属性时,TypeScript将其认为是一个带判别式的联合类型,并且可以缩小其成员。
interface Circle {
kind: "circle",
radius: number;
}
interface Square {
kind: "square",
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === 'circle') {
// shape -> Circle
console.log(shape.radius);
} else {
// shape -> Square
console.log(shape.sideLength);
}
}
如上,kind是联合类型共同的属性,当检查kind属性是否为circle时,从Shape类型中移除了所有没有kind或者kind不是circle的成员,类型从Shape缩小为Circle。
never类型
当进行类型缩小时,你可以将一个联合类型的选项减少到只剩下一种类型,即没有其他可能性。在这些情况下,TypeScript 将使用never类型来表示一个本不应存在的状态。
穷尽性检查
never类型可以分配给其它类型,但其它类型都不能分配给never(除了never本身)。这意味着可以使用类型缩小并依靠never进行穷进性检查:
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
// shape -> Circle
console.log(shape.radius);
break
case 'square':
// shape -> Square
console.log(shape.sideLength);
break
default:
// shape -> never
console.log(shape);
}
}
如果此时再增加一种新的联合类型,则会导致错误。