文章目录
- 一、基本用法
- 二、常见用途
- 2.1、将一个联合类型断言为其中一个类型
- 2.2、将一个父类断言为更加具体的子类
- 2.3、将任何一个类型断言为 any
- 2.4、将 any 断言为一个具体的类型
- 三、用法对比
- 3.1、类型断言 vs 类型转换
- 3.2、类型断言 vs 类型声明
- 3.3、类型断言 vs 泛型
一、基本用法
- TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。
- TypeScript 类型断言用来告诉编译器你比它更了解这个类型,并且它不应该再发出错误。
- 类型断言的一个常见用例是当你从 JavaScript 迁移到 TypeScript 时:
// 这里的代码发出了错误警告,因为 foo 的类型推断为 {},即没有属性的对象。
// 因此,你不能在它的属性上添加 bar 或 bas.
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'
- 上面这种问题就可以通过类型断言来避免:
interface Foo {
bar: number;
bas: string;
}
const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';
- 为了一致性,建议使用
as foo
的语法来为类型断言,而不是<foo>
语法。
// 最初的断言语法如下所示:
let foo: any;
let bar = <string>foo; // 现在 bar 的类型是 'string'
// 然而,当你在 JSX 中使用 <foo> 的断言语法时,这会与 JSX 的语法存在歧义:
let foo = <string>bar;</string>;
- 双重断言:
function handler(event: Event) {
// Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
const element = event as HTMLElement;
}
/*
如果你仍然想使用那个类型,你可以使用双重断言。
首先断言成兼容所有类型的 any,编译器将不会报错.
*/
function handler(event: Event) {
const element = (event as any) as HTMLElement; // ok
}
二、常见用途
2.1、将一个联合类型断言为其中一个类型
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
/*
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,
我们只能访问此联合类型的所有类型中共有的属性或方法!
*/
function getName(animal: Cat | Fish) {
return animal.name;
}
/*
我们需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如
*/
function isFish(animal: Cat | Fish) {
// 获取 animal.swim 的时候会报错
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
/*
此时可以使用类型断言,将 animal 断言成 Fish,从而解决报错
*/
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
- 类型断言只能够「欺骗」
TypeScript
编译器,无法避免运行时的错误
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
// 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了
(animal as Fish).swim();
}
/*
swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,
由于 Cat 上没有 swim 方法,就会导致运行时错误了
*/
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
// 而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误
swim(tom);
/*
该例子编译时不会报错,但在运行时会报错:
Uncaught TypeError: animal.swim is not a function`
*/
2.2、将一个父类断言为更加具体的子类
- 当类之间有继承关系时,类型断言也是很常见的:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
// 由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,
// 需要使用类型断言获取 (error as ApiError).code
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
- 使用
instanceof
不一定合适,虽然类能够通过instanceof
来判断error
是否是它的实例 - 但是有的情况下
ApiError
和HttpError
不是一个真正的类,而只是一个TypeScript
的接口(interface
)
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
// 会报错
if (error instanceof ApiError) {
return true;
}
return false;
}
2.3、将任何一个类型断言为 any
- 当我们引用一个在此类型上不存在的属性或方法时,就会报错:
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
- 但是我们是需要将
window
上添加一个属性foo
,并使他不报错
// 此时我们可以使用 as any 临时将 window 断言为 any 类型
(window as any).foo = 1;
2.4、将 any 断言为一个具体的类型
在日常的开发中,我们不可避免的需要处理 any 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。
- 比如:历史遗留的代码中有个
getCacheData
,它的返回值是any
function getCacheData(key: string | number): any {
return key;
}
- 这时候最好能够将调用了它之后的返回值断言成一个精确的类型
function getCacheData(key: string | number): any {
return key;
}
getCacheData('tom') as string;
三、用法对比
3.1、类型断言 vs 类型转换
- 类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值为 1
- 在上面的例子中,将
something
断言为boolean
虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
function toBoolean(something) {
return something;
}
toBoolean(1);
// 返回值为 1
- 若要进行类型转换,需要直接调用类型转换的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值为 true
- 总结:类型断言不是类型转换,它不会真的影响到变量的类型
3.2、类型断言 vs 类型声明
- 比如限制
tom
类型为Cat
// 使用类型断言写法
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
// 使用类型声明写法
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom: Cat = getCacheData('tom');
tom.run();
- 区别
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
// 使用类型断言写法
let tom = animal as Cat;
/*
分析:
animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可
由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom
*/
// 使用类型声明写法
let tom: Cat = animal; // 这时会报错
/*
分析:
animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行
这很容易理解,Animal 可以看作是 Cat 的父类,当然不能将父类的实例赋值给类型为子类的变量
*/
- 总结:为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅
3.3、类型断言 vs 泛型
- 还是这个例子:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
- 我们还有第三种方式可以解决这个问题,那就是泛型:
/*
通过给 getCacheData 函数添加了一个泛型 <T>,
我们可以更加规范的实现对 getCacheData 返回值的约束,
这也同时去除掉了代码中的 any,是最优的一个解决方案。
*/
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();