当 typescript 下的变量类型被定义为 any
unknown
Unions
甚至 Generics
时,我们对变量进行操作前需要知道其具体的类型,这种情况下,类型收窄 (Narrowing Types)可以提供帮助
举个例子 🌰 :
function isString(val: any) {
return typeof val === 'string'
}
function test(val: string | number){
if (isString(val)) {
console.log(val.toUpperCase()) // 1
} else {
console.log(val.toFixed(2)) // 2
}
}
上面的例子中,我们期望通过类型判断,让 typescript 在条件分支中能正确识别类型,这就是类型收窄的初衷。
然而,上面的代码事与愿违了,编辑器会在:
// 1 处报:
// Property 'toUpperCase' does not exist on type 'string | number'.
// 2 处报:
// Property 'toFixed' does not exist on type 'string | number'.
原因在于 typescript 是做静态分析检查的,而例子中val
的值只有在运行时才能确定具体的类型,所以我们寄希望于动态运行的代码影响静态检查可以说是异想天开了。
那怎么办呢?typescript 提供了两种方案:type guards (类型守卫) 与 assertions function (断言函数)
Type Guards
typeof
function test(val: string | number){
if (typeof val === "string") { // 1
console.log(val.toUpperCase())
} else { // 2
console.log(val.toFixed(2))
}
}
这样,在静态检查阶段就能知道进入 1 分支后类型一定是 string
,由此,进入 2 分支则类型就可能是 number
了。
typeof
guard 中,类型可以是:string
number
boolean
undefined
symbol
bigint
object
function
instanceof
function test(val: string[] | Promise<string>){
if (val instanceof Array) {
console.log(val.length)
} else {
console.log(val.then())
}
}
这个很容易理解
equal
function test(a: string | number, b: string | boolean) {
if (a === b) {
// a 和 b 严格相等的话, 那 a 和 b 必然都为 string 类型
a.toUpperCase()
}
}
虽然 ==
也可以做类型守卫,但 typescript 有个至今未修的问题,不清楚算不算 bug:
function test(a: string | number, b: string | boolean) {
if (a == b) {
// a 和 b 非严格相等,由于存在类型转换, a 与 b 并不一定都是 string 类型
a.toUpperCase()
}
}
test(1, true); // runtime error
最后执行test(1, true)
静态检查通过,但运行时确报错了。
另外: !==
与 !=
也可以做 type guard
strictNullChecks
启用 typescript 的 strictNullChecks
选项后,以下方式也能 type guard
:
function test(val?: string | numm) {
if (val == null) return;
// 现在,val 只可能是 string 类型
val.toUpperCase()
}
in
interface Bird {
fly(): void
}
interface Fish {
swim(): void
}
function test(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly()
} else {
animal.swim()
}
}
Array.isArray
function test(arr?: string[]) {
if (Array.isArray(arr)) {
Array.push('Hello World')
}
}
type predicates
当我们期望自定义类型守卫方法时便需要type predicates(类型谓词)
了, 之前那段代码中的isString
函数,稍加修改:
// 添加 val is string
function isString(val: any): val is string{
return typeof val === 'string'
}
function test(val: string | number){
if (isString(val)) {
console.log(val.toUpperCase()) // 1
} else {
console.log(val.toFixed(2)) // 2
}
}
通过在isString
的返回类型声明处添加val is string
,告知编辑器该函数返回true
时,val
一定是 string
类型,这样,编辑器在静态分析阶段就能在条件分支中推断变量类型了。
在 callback 中的问题
type People = {
name: unknown
}
function test(people: People) {
if (typeof people.name === 'string') {
people.name.toUpperCase();
[].forEach(() => {
people.name.toUpperCase(); // Error: Object is possibly 'undefined'.
});
people.name.toUpperCase();
}
}
明明已经收窄了类型,为什么在回调中依旧会出错呢?
因为编辑器也拿不准回调函数会被同步还是异步执行,如果被异步执行,people
就脱离了type guard
所能管辖的区域了,在当前分支之外,可以给其赋值为其他类型。
那怎么解决?
type People = {
name: unknown
}
function test(people: People) {
if (typeof people.name === 'string') {
people.name.toUpperCase();
// peopleName 在分支所在作用域内,且类型确定为 string
const peopleName = people.name;
[].forEach(() => {
// 分支之外不可能修改 peopleName
peopleName.toUpperCase();
});
people.name.toUpperCase();
}
}
Assertions Function
asserts «cond»
function assertTrue(condition: boolean, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
function test(val: unknown) {
assertTrue(typeof val === 'string', `${val} is not string type`)
// 现在 val 只会是 string 类型
val.toUpperCase()
}
asserts «arg» is «type»
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') throw TypeError()
}
function test(val: number | string) {
assertIsString(val)
// 现在 val 只会是 string 类型
val.toUpperCase()
}
使用场合
以上类型收窄的演示我们都在是 if
判断中,也可以在:
switch...case
function test(val: number | string) {
switch(typeof val) {
case 'string':
val.toUpperCase();
break;
case 'number':
val.toFixed();
break;
}
}
Array.prototype.filter
const arr: unknown[] = [1, 'hello', null];
const ret = arr.filter((item): item is string => typeof item === 'string');
上例中,ret
会被推断为 string[]
最后
如何实现一个万能的 isTypeof 函数,一个参考:
function isTypeof<T>(val: unknown, typeVal: T): val is T {
if (typeVal === null) return val === null;
return val !== null && (typeof val === typeof typeVal)
}
function test(val: unknown) {
if (isType(val, '....something')) {
// now, val is string
val.toUpperCase()
}
}
当然,上例中别扭的地方在于isTypeOf
的第二个参数实际只是做类型标识,并无其他用处,改进版本:
function isTypeOf(val: unknown, type: 'string'): val is string
function isTypeOf(val: unknown, type: 'boolean'): val is boolean
function isTypeOf(val: unknown, type: 'number'): val is number
function isTypeOf(val: unknown, type: string): boolean {
return typeof val === type
}
3 条回复
[…] 发布了 4.4 Beta 版本,添加以下新特性: 增强了 Narrowing Types […]