在平时的 typescript
开发中,有时你比 ts
编译器更理解一个值对应的类型,通常是将编译器已推断类型向窄化的方向发展。这个时候你就会使用类型断言。下面是官网给出的语法格式:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
复制代码
另一个为as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
复制代码
类型断言的限制
既然类型断言如此强大可以推翻编译器对一个值类型的已有推论,这种危险技法必然有某种限制。
并不是所有的类型之间都可以进行类型断言,前提是两个类型之间要有 (父子/超子)类型关系
,详细的来说可以分为一下两种情况:
- 如果两个类型互相兼容,即
A
兼容B
,B
也兼容A
。那么A
能够被断言为B
,B
也能被断言为A
。- 这种情况的任意一个断言方向都可以被称之为安全的双向推断。
- 除此之外,若
A
兼容B
,但是B
不兼容A
,此情况下A
也能够被断言为B
,B
也能被断言为A
。- 这种情况下,将
A
断言为B
,称之为不安全的类型窄化。 - 将
B
断言为A
,称之为安全的类型泛化(个人叫法)。
- 这种情况下,将
下面我们通过一个简化的例子,来理解类型断言的限制:
interface Animal {
type: string;
}
interface Cat {
type: string;
miao(): void;
}
let tom: Cat = {
type: 'land_animal'
miao: () => console.log('miao');
};
let animal: Animal = cat;
复制代码
我们知道,TypeScript
是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。此时 cat
拥有 Animal
的全部属性,可以理解成它们的结构与 Cat extends Animal
是等价的。
此时我们就可以说 Animal
兼容 Cat
了,当 Animal
兼容 Cat
时,它们就可以互相进行类型断言了:
interface Animal {
type: string;
}
interface Cat {
type: string;
miao(): void;
}
function cookAnimal(animal: Animal) {
return (animal as Cat);
}
function cookCat(cat: Cat) {
return (cat as Animal);
}
复制代码
这样的设计其实也很容易就能理解:
- 允许
animal as Cat
是因为「父类可以被断言为子类」,相当于是不太安全的类型窄化。 - 允许
cat as Animal
是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,故「子类可以被断言为父类」,相当于是安全的类型泛化。
需要注意的是,这里我们使用了简化的父类子类的关系来表达类型的兼容性,而实际上 TypeScript
在判断类型的兼容性时,比这种情况复杂很多,下面是官往上给出的类型兼容性关系表:
值\变量 | any | unknown | object | void | undefined | null | never |
---|---|---|---|---|---|---|---|
any → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
unknown → | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
object → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
void → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
undefined → | ✓ | ✓ | — | ✓ | — | ✕ | |
null → | ✓ | ✓ | — | — | — | ✕ | |
never → | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
列表示变量(常量)的类型,行表示要分配给变量(常量)的值。 “—
”表示仅在--strictNullChecks
关闭时才兼容的类型。
-
任何类型的值都可以分配给其对应类型的变量。
-
any
和unknown
可以看做是所有类型的超类,作为变量的时候接受任何类型的值。不过作为值得时候稍有不同:any
作为值可以分配给任何类型;unknown
只能分配给any
;
-
never
是所有类型的子类型,值为never
类型可以赋给所有类型的变量。但是反之never
变量只能接受never
。 -
void
变量接受any
、undefined
、never
。但是作为值只能赋给any
、unknown
。 -
undefined
作为变量可以接受any
和never
,作为值可以赋给any
,unknown
、void
。 -
null
和undefined
的行为差不多,只是不能赋个void
。
接下来我们用几个实际的例子来理解。
undefined 和 void
在类型兼容性中官方这样阐述二者关系:
undefined
作为变量可以接受any
和never
,作为值可以赋给any
,unknown
、void
。
也就是说 undefined
是 void
的子类型,二者之间有超子关系,于是类型断言的条件就满足了:
const vod = () => {};
vod() as undefined; // 类型窄化
let undef: undefined;
undef as void; // 类型泛化
复制代码
any、unknown 和 任何类型
类型兼容性中有这样的说明:
any
和unknown
可以看做是所有类型的超类,作为变量的时候接受任何类型的值。不过作为值得时候稍有不同:
any
作为值可以分配给任何类型;unknown
只能分配给any
;
总结一下,any
和所有类型互相兼容,unknown
是所有类型的超类,两个类型分别和所有类型满足断言关系,于是就有这样的写法:
// 任何类型 as unknown as 任何类型
// 任何类型 as any as 任何类型
let a = 1;
a as any as string
a as unknown as string
复制代码
这样做当然是不对的,利用了类型断言的漏洞,不过在某些特殊的情况下非常有用。
联合类型
联合类型是联合成员的父类型,所以任何联合类型都可以被推断为其成员,其成员也可以逆推为联合类型。
举一个很有趣的例子:
这个例子里,我们将
Dog
推断为 Cat | Dog
又推断为 Cat
,这是一种 hack
的做法,Dog
直接推断为 Cat
是不行的,联合类型的成员之间不可以互推。
函数类型
首先我们要知道函数类型要兼容,函数参数必须是逆变的,返回值必须是协变的,那么是不是满足函数兼容性两个就可以互推呢?
来看看下面的例子,还是上面例子里的 Cat
和 Dog
,首先 CDToCD
赋值给 CToCD
满足参逆返协,这 ok。而 CToCD
赋值给 CDToCD
则不满足(刚好反了),所以这样写是不兼容的:
如何让第二行代码跑通不报错呢,很简单,两次断言:
type CDToCD = (c: Cat | Dog) => Cat | Dog;
type CToCD = (c: Cat) => Cat | Dog;
const fn1: CDToCD = ((cd: Cat) => ({ miao() {} })) as CToCD as CDToCD;
复制代码
双重断言
前面的例子里我们使用好几个双重断言了,实际开发中真的可以利用这个技巧来完成一些编译器认为不可以兼容,但是你认为可以兼容的类型推断,既然:
- 任何类型都可以被断言为
any|unknown
any|unknown
可以被断言为任何类型
那么我们可以使用双重断言 as any as Foo
来将任何一个类型断言为任何另一个类型。 比如在 react-use
中由于被 useCallback
包裹的函数返回的类型不能被正确推断,于是就是用了这个技巧:
const useAsyncFn = <T extends FuncReturnPromise>(
fn: T,
deps: DependencyList = []
): AsyncFnReturn<T> => {
const [asyncState, setAsyncState] = useState<
StateFromFunctionReturningPromise<T>
>({
loading: false,
});
const isMount = useMountedState();
const fetch = useCallback((...args: Parameters<T>): ReturnType<T> => {
!asyncState.loading &&
setAsyncState({
loading: true,
});
return fn(...args)
.then((res) => {
isMount() && setAsyncState({ loading: false, value: res });
})
.catch((err) => {
isMount() && setAsyncState({ loading: false, error: err });
}) as ReturnType<T>;
}, deps);
return [asyncState, fetch as unknown as T];
};
复制代码