【TypeScript入门】基础(七):类型断言

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

语法

<as 类型> 或者 <类型>

在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型

<Foo> 的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型

故建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法。

类型断言的作用

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

将一个联合类型断言位其中的一个类型

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

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.swim 的时候会报错。

此时可以使用类型断言,将 animal 断言成 Fish

 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;
 }

这样就可以解决访问 animal.swim 时报错的问题了。

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

 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 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 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

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;
}

上面例子中我们用instanceof 确实更合适,ApiError 是一个JavaScript类,能够通过 instanceof 来判断 error 是不是他的实例。

但是有的情况下, ApiErrorHttpError 不是一个真正的类,而只是一个 TypeScript 的接口,接口是一个类型而不是一个真正的类,他在编译结果中会被删除,所以无法继续使用 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;

// Property 'length' does not exist on type 'number'.

因为数字类型的变量foo 不存在 length 属性,所以在编译的时候会做出错误提示,这种错误提示是非常有用的,但是有时候我们非常确认代码不会出错,比如:

window.foo = 1;
// Property 'foo' does not exist on type 'Window & typeof globalThis'.

上面的例子中,我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。

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

(window as any).foo = 1;

注意:

  • any 类型的变量上,访问任何属性都是允许的。

  • 将一个变量断言为 any 类型可以说是解决 typescript 中类型问题的最后一个手段,它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any

  • 一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。

any 断言为一个具体的类型

在日常的开发中,我们不可避免的需要处理 any 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。

遇到 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 的访问时就有了代码补全,提高了代码的可维护性。

类型断言的限制

从上面例子中我们可以总结到:

  • 联合类型可以被断言成其中一个类型;
  • 父类可以被断言为子类;
  • 任何类型都可以被断言为any;
  • any可以被断言成任意类型;

所以,类型断言有什么限制呢?是不是任何类型都可以被断言为任何另外类型呢?当然不行–并不是任何一个类型都可以被断言为任何另一个类型的。

若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;

// 编译后js代码:
var Tom = {
    name: 'Tom',
    run: function () { console.log('run'); }
};
var animal = Tom;

我们知道,TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。

在上面的例子中,Cat 包含了 Animal 中的所有属性,除此之外,它还有一个额外的方法 run。TypeScript 并不关心 CatAnimal 之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Cat extends Animal 是等价的:

// 上面例子等价于下面
interface Animal {
    name: string;
}
interface Cat extends Animal {
    run(): void;
}
let Tom: Cat = {
  name: 'Tom',
  run: () => { console.log('run') }
}

let animal: Animal = Tom;

那么也不难理解为什么 Cat 类型的 Tom 可以赋值给 Animal 类型的 animal 了——就像面向对象编程中我们可以将子类的实例赋值给类型为父类的变量。TypeScript 中更专业的说法,即:Animal 兼容 Cat

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 可以被断言为任何类型

那么我们是不是可以使用双重断言 as any as Foo 来将任何一个类型断言为任何另一个类型呢?

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 兼容 BB 兼容 A 即可」的限制,将任何一个类型断言为任何另一个类型。

若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。

除非迫不得已,千万别用双重断言。

类型断言 vs 类型转换

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

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

toBoolean(1);
// 返回值为 1

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

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

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

toBoolean(1);
// 返回值为 true

类型断言 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();

我们使用 as Catany 类型断言为了 Cat 类型。

但实际上还有其他方式可以解决这个问题:

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

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

const tom: Cat = getCacheData('tom');
tom.run();

上面的例子中,我们通过类型声明的方式,将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom

这和类型断言是非常相似的,而且产生的结果也几乎是一样的——tom 在接下来的代码中都变成了 Cat 类型。

那么他们有什么区别呢?我们通过下面例子来理解:

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

const animal: Animal = {
    name: 'tom'
};
let tom = animal as Cat;

在上面的例子中,由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom

但是若直接声明 tomCat 类型:

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

const animal: Animal = {
    name: 'tom'
};
let tom: Cat = animal;
//  Property 'run' is missing in type 'Animal' but required in type 'Cat'.

则会报错,不允许将 animal 赋值为 Cat 类型的 tom

Animal 可以看作是 Cat 的父类,当然不能将父类的实例赋值给类型为子类的变量。

深入的讲,它们的核心区别就在于:

  • animal 断言为 Cat,只需要满足 Animal 兼容 CatCat 兼容 Animal 即可
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行

但是 Cat 并不兼容 Animal。前一个例子中,是由于 getCacheData('tom')any 类型,他们互相兼容所以不报错。

知道了它们的核心区别,就知道了类型声明是比类型断言更加严格的。

所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。

类型断言 vs 泛型

function identity<T>(arg: T): T {
    return arg;
}

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。我们后面会专门的介绍泛型。

还是这个例子:

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<T>(key: string): T {
    return (window as any).cache[key];
}

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

const tom = getCacheData<Cat>('tom');
tom.run();

通过给 getCacheData 函数添加了一个泛型 <T>,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

small_Axe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值