类型守卫
我们可以把类型守卫的逻辑判断当作是一条流过的河流。
declare const strOrNumOrBool: string | number | boolean;
if (typeof strOrNumOrBool === "string") {
// 一定是字符串!
strOrNumOrBool.charAt(1);
}
但是如果我们把 typeof 的类型判断单独抽离出来,会发现情况有所不同。
function isString(input: unknown): input is string {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 类型 string | number 上不存在属性 replace
(input).replace("aaa", "bbb")
}
if (typeof input === 'number') { }
// ...
}
想象类型控制流分析这条河流,刚流进 if (isString(input))
就戛然而止了。因为 isString
这个函数在另外一个地方,内部的判断逻辑并不在函数 foo
中。这里的类型控制流分析做不到跨函数上下文来进行类型的信息收集。
但是将判断逻辑封装起来提取到函数外部进行复用非常常见。为了解决这一类型控制流分析的能力不足, TypeScript 引入了 is
关键字来显式地提供类型信息。
function isString(input: unknown): input is string {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 正确
(input).replace("aaa", "bbb")
}
if (typeof input === 'number') { }
// ...
}
isString
函数称为类型守卫,在它的返回值中,我们不再使用 boolean 作为类型标注,而是使用 input is string
的搭配。
input is string
拆开分析含义是:
input
,函数的某个参数is string
,即is
关键字 + 预期类型,即如果这个函数成功返回为 true,那么is
关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。
从这个角度来看,其实类型守卫有些类似于类型断言,但类型守卫更宽容,也更信任你一些。你指定什么类型,它就是什么类型。
下面是两个开发常用的类型守卫。
export type Falsy = false | "" | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined
export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);