TypeScript 类型收窄
写这篇文章是因为最近在写一个管理异步队列的库,但是有一些类型推导不太好写,例如上面的期待是
if (values.stauts === 'error') {
// values.data to Error
}
后面查询了一些文档和资料就有了这篇文章。
收窄方式
在具体讲解上面如何实现之前,先介绍一下几种收窄对象的方式。
typeof 类型守卫
function padLeft(padding: number | string, input: string): string {
throw new Error('Not implemented yet!');
}
如果在 padLeft 函数内调用不是 stirng 和 number 相同的方法,例如 toString、valueOf 会提示方法不存在。
之所以出现这样问题是因为 Typescript 并不知道 padding 到底是 number 还是 string,但是你可以通过 typeof 的显式告诉它的类型。
function padLeft(padding: number | string, input: string): string {
if (typeof padding === 'string') {
return padding.charAt(0);
}
// 排除了string,剩下就是number
return padding.toFixed(2);
}
条件语句
在使用条件语句,例如 if
、&&
、||
、!
等条件语句的时候也会缩小类型。
看一个例子
function printAll(strs: string | string[] | null) {
//
}
按照上面 typeof 语句很自然就想到判断是不是 object 来完成数组区分
function printAll(strs: string | string[] | null) {
if (typeof strs === 'object') {
strs.forEach((item) => {});
// Object is possibly 'null'.
//'item' is declared but its value is never read.
}
}
不过写到一半就会发现提示报错了,strs 在 object 的判断下依然可能为 null,回忆一下 typeof 的运算符可以得知这个是正常现象,null 的 typeof 返回值也是 object,这个是 JavaScript 语言设计的一个缺陷。
不过要怎么消除这个影响呢?这就用到这条件语句,可以通过判断 strs 是否存在显式排除 null 的存在
function printAll(strs: string | string[] | null) {
if (typeof strs === 'object' && strs) {
// object
return strs;
// 这里strs类型已经被排除object了
} else if (typeof strs === 'string') {
return strs;
}
// null
return strs;
}
相等运算符和 switch 收窄
还可以使用 ===
、==
、!=
、!==
运算符来完成收窄
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {}
例如上面,如果想让 container 排除 null 和 undefined 就可以使用 ===
来完成,下面是示例
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
if (container.value === undefined) {
return 0;
}
if (container.value === null) {
return 1;
}
return container.value === factor;
}
通过 === 完成了 undefined 和 null 的收窄过程,这里顺便说下其实上面的写法可以简写成 ==
,== 在判断 undefined 跟 null 之间时返回 true。
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
if (container.value == undefined) {
return 0;
}
return container.value === factor;
}
in 运算符
in 运算符用于判断一个属性是否存在对象上,在 TypeScript 同样可以使用 in 来完成收窄。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
return animal.swim();
}
return animal.fly();
}
上面 move 通过指定 swim in animal
告诉 TypeScript,在 if 中类型就是 Fish,因为 Bird 中不存在这个属性。
使用 in 运算符收窄时,使用其他联合类型不存在的属性来完成对某一类型的收窄
instanceof
与 JavaScript 中一样,instanceof 也可以判断某个属性是否匹配对象。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
赋值
这里直接看例子
let x = Math.random() < 0.5 ? 10 : 'hello world!';
// let x: string | number
x = 1;
console.log(x);
// let x: number
x = 'goodbye!';
console.log(x);
// let x: string
不过注意,分配给上面的 x 一定要符合 x 最初的联合类型,也就是说给定 boolean 会报错。
类型谓词
在使用 lodash 类型的函数库,通常都会有 isObject,你可能很好奇怎么实现。
其实它们就是使用了类型谓词,下面动手实现一个 isObject
function isObject (value:any) : value is object {
return value && typeof value === 'object'
}
之后在配合使用就可以完成类型收窄
function test(pet: object | null) {
if (isObject(pet)) {
return pet;
} else {
return {};
}
}
interface 收窄
上面将收窄的方式列举了一番,下面就看下在 interface 中如何实现收窄,最开始已经说了,我们期待使用 if 条件语句可以通过 status 的不同来完成 data 的变化。
在 TypeScript 我们可以使用联合类型将两者通用部分区分
export interface ChangeError {
status: 'error';
data: Error;
}
export interface ChangeData<T> {
status: 'success';
data: T;
}
之后拼接一起
export type Change<T = any> = {
index: number,
progress: number,
total: number,
} & (ChangeError | ChangeData<T>);
之后通过 TypeScript 的条件收窄就自动完成这一过程。
收窄类型的实用示例
收窄 + never 结合可以完成一个联合类型添加新对象不处理报错。
interface A {
name: 'a';
}
interface B {
name: 'b';
}
type Types = A | B;
function check(values: Types) {
switch (values.name) {
case 'a':
return '';
case 'b':
return '';
default:
const _never: never = values;
return _never;
}
}
never 表示永远为空的类型,结合 switch 可以将对象类型收窄尽,这时对象的类型就是 never。
现在如果给 Types 添加新的联合类型 default 就会抛出错误
interface A {
name: 'a';
}
interface B {
name: 'b';
}
type Types = A | B | { name: 'c' };
function check(values: Types) {
switch (values.name) {
case 'a':
return '';
case 'b':
return '';
default:
const _never: never = values;
return _never;
}
}