on
TS思考 - Type Guard
为什么突然要学type guard
呢?
突然有一天被问到了type guard
是什么?
在写Table的时候其实很多地方都用了只是自己不太知道这个就是type guard
。
那么今天就来好好的了解一下什么是type guard
。
一个场景
interface A {
name: string;
code: number;
}
interface B {
name: string;
birthday: Date;
}
type Test = A | B;
function Func(param: Test) {
if (param.code) {
// error: Property 'code' does not exist on type 'B'.
console.log(param.code);
}
}
其实这个场景很多情况下都会存在,一个union
类型的变量想要用其中一个分支的属性,但是这个时候就会报错。
narrowing(类型收紧) 和 type guard(类型保护)是什么关系?
其实最开始迷惑这两个概念是因为typescript
的官方文档。
在网上搜索到的一些关于type guard
的文章,给出的官方文档链接都清一色的指向了这个旧文档。
而新的文档中,能找到的关于type guard
的内容仅在narrowing
的章节中可以看到。
难道是type guard
被narrowing
取代了吗?
个人认为:
type guard
的本质其实就是在当前分枝收紧类型的范围,让类型更近则TS可以推断出更详细的内容。
所以narrowing所做的操作,是包含type guard
的相关操作,且更丰富了。
盲猜这也是官方把
type guard
的文档废弃而编写了narrowing
的文档的原因。
typeof type guards
function padLeft(padding: number | string, input: string) {
return new Array(padding + 1).join(' ') + input;
// Operator '+' cannot be applied to types 'string | number' and 'number'.
}
上面这个例子中padding + 1
这个操作对于string
或者number
来说是有不同的结果的。所以TS告诉我们,你确定要这么做吗?
function padLeft(padding: number | string, input: string) {
if (typeof padding === 'number') {
return new Array(padding + 1).join(' ') + input;
}
return padding + input;
}
通过typeof
来判断当前参数padding
的类型,从而可以通过产生不同的分支来收紧类型。
这个过程我们其实是在narrow the type.
但是是不是和我们之前说的通过typeof
来完成的type guard
一摸一样。
和通过typeof
来建立不同分支,在每个分支中类型都更紧的这种操作类似的还有
Truthiness narrowing
一说到if来创建条件语句,那么就离不开true
or false
。
JavaScript中有很多的值会被转为false
- 0
- NaN
- ”” (the empty string)
- 0n (the bigint version of zero)
- null
- undefined
同样是创建分支来完成类型收紧,排除可能为false
的值,来进行后续的操作。
in操作符
个人认为,in
操作符其实就是判断当前这个类型中有无此属性。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
上述这个例子,通过判断"swim"
属性是否在当前变量中,来让TS自己推断出具体的类型。因为swim可以唯一标识
一个类型。
不得不说的 never
我们还使用一下开头那个例子
interface A {
name: string;
code: number;
}
interface B {
name: string;
birthday: Date;
}
type Test = A | B;
function Func(param: Test) {
if ('code' in param) {
console.log(param.code);
} else if ('birthday' in param) {
console.log(param.birthday);
} else {
console.log(param);
}
}
其实在这个例子中,param类型为Test
, 本质上就是A | B
。
我们在Func函数中给出了3
个分支。一个是narrowing到了A
,另一个是B
。
其实至此为止,param的全部情况的类型都已经被TS推断出来了。
但是这个时候最终的else
分支是一定不会走到的
这个时候TS就会认为这个分支中的param类型为never
。
TS 文档给出的never的定义:
The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.
大致翻译一下就是:
类型为never
的变量可以赋值给任意类型的其他变量,但是只有never
类型的变量可以赋值给never
类型。
你可以利用never
来检查是否narrowing的每种类型都走过了(如果没有走全是不会出现never
的)。
wait a minute… exhaustive checking
这是个撒子?
Exhaustiveness checking
exhaust
有精疲力尽的,用尽
的意思。
其实就如同上面never
章节所说到的,如果没有把当前变量
类型的所有情况都考虑到,TS是不可能推断出当前变量
的类型为never
。
所以当分支能让TS类型推断出never
那说明类型检查真的走到头了。