TypeScript 入门自学笔记 — 类型断言(二)

码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14558034.html

类型断言

类型断言(Type Assertion): 主要用于当 TypeScript 推断出来类型并不满足当前需求时,TypeScript 允许开发者覆盖它的推断,可以用来手动指定一个值的类型。

类型断言是一个编译时语法,不涉及运行时。

语法

值 as 类型(推荐)<类型>值

形如 <Foo> 的语法在 ts 中除了表示类型断言之外,也可能是表示一个泛型,故建议在使用类型断言时,使用 值 as 类型 语法。

类型断言的用途

类型断言的常见用途有以下几种:

联合类型可以被断言为其中一个类型

上一篇文章中介绍访问联合类型的属性和方法,当 TS 不确定一个联合类型的变量到底是哪个类型时,只能访问此联合类型的所有类型*有的属性或方法

interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
} // 接口可看作一种类型
function getName(animal: Cat | Fish) {
return animal.name;
}

而有时确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,如:

interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
} function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') { // 报错
return true;
}
return false;
} // Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.

上述报错可使用类型断言,将 animal 断言成 Fish,就可以解决访问 animal.swim 报错的问题。

interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
} function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}

注意:类型断言只能够欺骗 TS 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
} function swim(animal: Cat | Fish) {
(animal as Fish).swim();
} const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// 编译时不会报错,但在运行时会报错 Uncaught TypeError: animal.swim is not a function`

原因是 (animal as Fish).swim() 这段代码将 animal 直接断言为 Fish ,隐藏了 animal 可能为 Cat 的情况,而 TS 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。

可是 swim 函数接受的参数类型是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误。

总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

父类可以被断言为子类

当类之间有继承关系时,类型断言也很常见:

class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
} function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}

声明了函数 isApiError,用来判断传入的参数是不是 ApiError 类型,其参数的类型肯定得是父类 Error,因此该函数便可接受 Error 或它的子类作为参数。

但由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,需要使用类型断言获取 (error as ApiError).code

此案例中有一个更合适的方式来判断是不是 ApiError,那就是使用 instanceof, 因为 ApiError 是一个 JavaScript 的类,能够通过 instanceof 来判断 error 是否是它的实例:

class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
} function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}

但有些情况下 ApiErrorHttpError 不是一个真正的类,而只是一个 TS 的接口(interface),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,就无法使用 instanceof 来做运行时判断了:

interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
} function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
} // 'ApiError' only refers to a type, but is being used as a value here.

此时就只能用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError

interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
} function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
任何类型都可以被断言为 any

理想情况下,每个值的类型都具体而精确,但引用一个该类型上不存在的属性或方法,就会报错:

const foo: number = 1;
foo.length = 1; // 数字类型上是没有 `length` 属性
// Property 'length' does not exist on type 'number'.

而有时,非常确定一段代码不会出错,如给 window 上添加一个属性 foo,但 TS 编译时会报错,提示 window 上不存在 foo 属性。:

window.foo = 1;

// Property 'foo' does not exist on type 'Window & typeof globalThis'.

此时可以使用 as any 临时将 window 断言为 any 类型,在 any 类型的变量上,访问任何属性都是允许的。

(window as any).foo = 1;

将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any总之,一方面不能滥用 as any,另一方面也不要完全否定它的作用,需要在类型的严格性和开发的便利性之间掌握平衡

any 可以被断言为任何类型

在日常的开发中,不可避免的需要处理 any类型的变量,我们可以选择无视它,也可以选择改进它,通过类型断言及时的把 any 断言为精确的类型,亡羊补牢,提高代码可维护性。

function getCacheData(key: string): any {
return (window as any).cache[key];
}

在使用时,最好能够将调用了它之后的返回值断言成一个精确的类型,方便后续操作:

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 之后,立即将它断言为 Cat 类型。明确 tom 的类型,后续对 tom 的访问就有了代码补全,提高了代码的可维护性。

类型断言的限制

类型断言是有限制的,并不是任何一个类型都可以被断言为任何另一个类型。具体来说,A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A

interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
} let tom: Cat = {
name: 'Tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;

Cat 包含了 Animal 中的所有属性,TypeScript 只关注最终的结构有什么关系——所以同 Cat extends Animal 是等价的:

interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}

这也是为什么 Cat 类型的 tom 可以赋值给 Animal 类型的 animal

Animal 兼容 Cat 时,它们就可以互相进行类型断言

interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
} function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}
  • 允许 animal as Cat 是因为「父类可以被断言为子类」,这个前面已经学习过了
  • 允许 cat as Animal 是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,故「子类可以被断言为父类」

要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可,这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的。

综上所述:

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
  • 要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可

其实前四种情况都是最后一个的特例。

双重断言

既然:

  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
} function testCat(cat: Cat) {
return (cat as any as Fish);
}

若直接使用 cat as Fish 肯定会报错,因为 CatFish 互相都不兼容。

但是使用双重断言,则可以打破要使得A能够被断言为 B,只需A 兼容 B 或B兼容A即可的限制,可以使用双重断言 as any as Foo将任何一个类型断言为任何另一个类型

但是若使用了双重断言,那么很可能会导致运行时错误。除非迫不得已,千万别用双重断言。

类型断言 vs 类型转换

类型断言不是类型转换,它不会真的影响到变量的类型。

类型断言只会影响编译时的类型,断言语句在编译结果中会被删除:

function toBoolean(something: any): boolean {
return something as boolean;
} toBoolean(1);
// 返回值为 1

若要进行类型转换,还是需要调用类型转换的方法:

function toBoolean(something: any): boolean {
return Boolean(something);
} toBoolean(1);
// 返回值为 true

上一篇:TypeScript 入门自学笔记(一)



上一篇:Go interface 原理剖析--类型转换


下一篇:来自朝鲜的问候 golang入坑系列