类型断言

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-21

类型断言 (Type Assertion) 可以用来手动指定一个值的类型,语法是 值 as 类型<类型>值,在 tsx 语法中必须使用前者。

类型断言的用途

将一个联合类型断言为其中一个类型

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法

interface Cat {
  name: string;
  run(): void;
}
interface Fish {
  name: string;
  swim(): void;
}

function getName(animal: Cat | Fish) {
  return animal.name;
}

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

function isFish(animal: Cat | Fish) {
  return typeof animal.swim === 'function'; // error
}

就会报错,此时就可以使用类型断言:

function isFish(animal: Cat | Fish) {
  return typeof (animal as Fish).swim === 'function'; // success
}

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

function swim(animal: Cat | Fish) {
  (animal as Fish).swim();
}

const tom: Cat  = {
  name: 'Tom',
  run() { console.log ('run') }
}

swim(tom); // error, animal.swim is not function 

原因是 (animal as Fish).swim() 隐藏了 animal 可能是 Cat 的情况。

将一个父类断言为更加具体的子类

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

class ApiError extends Error {
  code: number = 0;
}

class HttpError extends Error {
  statusCode: number = 200;
}

// 用来判断传入的参数是不是ApiError类型
// 所以这个时候参数就应该是更抽象的父类Error了
// 这样这个函数就可以接受Error或它的子类作为参数了
function isApiError(error: Error) {
  return typeof (error as ApiError).code === 'number'
}

function isApiError(error: Error) {
  return error instanceof ApiError;
}

在此例中,使用 instanceof 判断传入的参数是否是 ApiError 的实例更为合适,因为 ApiError 是一个 JavaScript 的类。

但是有些情况下 ApiErrorHttpError 不是一个真正的类,而只是一个 TypeScript 的 接口interface,而接口是一个类型而不是一个真正的值,它在编译结果中会被删除,也就无法使用 instanceof 来判断。此时就只有使用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError 了。

interface ApiError extends Error {
  code: number;
}

interface HttpError extends Error {
  statusCode: number;
}

function isApiError(error: Error) {
  return typeof (error as ApiError).code === 'number'
}

将任何一个类型断言为any

有的时候,我们会非常确定这段代码不会出错,但是 TypeScript 会报错。比如:

window.foo = 1;
// 类型“Window & typeof globalThis”上不存在属性“foo”。

此时我们可以使用 as any 临时将 window 断言为 any

(window as any).foo = 1;

any 类型的变量上,访问任何属性都是允许的。但是它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any,总之不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡。

将any断言为一个具体的类型

遇到 any 类型的变量(第三方库未能定义后自己的类型、历史遗留或其他人编写的烂代码),我们可以无视它,任由它滋生更多的 any,也可以选择改进它,通过类型断言及时的把 any 断言为明确的类型。

// 返回any类型
function getCacheData(key: string): any {
  return (window as any).cache[key];
}

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

const tom = getCacheData('tom') as Cat; // 断言any为具体的 Cat 类型
tom.run();

类型断言的限制

类型断言是否存在限制呢?是不是任何一个类型都可以被断言为另一个类型呢? 答案是否定的。

具体来说,若 A 兼容 B,那么 A 能被断言为 B,B 也能被断言为 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 类型中的所有属性,并拥有一个额外的方法 run, 而 TypeScript 并不关心 Cat 和 Animal 被定义时是什么关系,而只是会看它们最终的结构有什么关系,所以在上例中是与 Cat extends Animal 是等价的。

这就像面向对象编程中的我们可以将子类的实例赋值给类型为父类的变量。在 TypeScript 中,则认为 Animal 兼容 Cat,而当它们兼容是,它们就可以互相进行类型断言了。

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兼容B 或 B兼容A 即可。

双重断言

上面提到过,任何类型都能断言为 any, any 又可以被断言为任何类型,那么是不是可以解决上面的问题,使用双重断言 as any as Foo,将任何一个类型断言为另一类型呢?

interface Cat {
  run(): void;
}

interface Fish {
  swim(): void;
}

function testCat(cat: Cat) {
  return cat as any as Fish;
}

使用双重断言企业确实可以实现这一点,但这是十分危险的行为。因为它打破了 A兼容B 或 B兼容A 的限制,这样 cat 之后调用 Fish 类型的 swim 方法就会报错。

类型断言 vs 类型转换

类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除。 所以类型断言不是类型转换,它并不会真的影响到变量的类型,若要进行类型转换,需要直接调用类型转换的方法。

function toBoolean(something: any): boolean {
  return something as boolean;
}

var result = toBoolean(1);
// 返回的仍然是1而不是boolean
// 尽管 result 在编译阶段会被当做 boolean类型 看待

// 直接使用类型转换
function toBool(something: any): boolean {
  return Boolean(something);
}

类型断言 vs 类型声明

上面提到的 将any断言为一个具体的类型 也可以类型声明的方式解决。

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

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

const tom: Cat = getCacheData('tom'); // 不再使用 as Cat
tom.run();

而类型声明和类型断言的区别是:

  • A类型的值断言为B类型,只需要满足 A兼容B 或 B兼容A 即可。
  • A类型的值赋值给B类型的变量,需要满足B兼容A才行。

这里的B兼容A,可以理解为B拥有A所有的属性和方法。

interface Animal {
  name: string;
}

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

const animal: Animal = {
  name: 'tom'
};

let tom = animal as Cat; // success
let tom2: Cat = animal; // fail 类型 "Animal" 中缺少属性 "run",但类型 "Cat" 中需要该属性。

这也很容易理解,Animal可以看作是Cat的父类,当然不能将父类的实例赋值给类型为子类的变量。而前一个例子中, getCacheData返回的类型是 any,显然 any 类型兼容 Cat,Cat 也兼容 any,故使用类型声明是可行的。

所以可以知道,类型声明比类型断言更加严格,所以最好优先使用类型声明。

类型断言 vs 泛型

还是 将any断言为一个具体的类型 这个例子,我们可以使用第三种方法解决,就是使用泛型,通过给 getCacheData 添加泛型,更加规范的实现对 getCacheData 返回值的约束。

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();

这样同时去掉了代码中的 any。