3、typeScript类型断言、函数fun

475 阅读10分钟

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

语法:

as 类型

//或者
<类型>值

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

所以:大家在使用类型断言时,建议使用 值 as 类型 这样的语法。

类型断言用途:

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

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

interface people {
  name: string; 
  run(): void;
}
interface fish {
  name: string; 
  swim(): void;
}

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

interface people {
  name: string; 
  run(): void;
}
interface fish { 
  name: string; 
  swim(): void;
}
function isPeople(animal: people | fish) {
  if (typeof animal.swim === ' function') {
    return true;
  }
    return false;
}
// error: Property ' swim' does not exist on type 'people| fish'./
//Property' swim' does not exist on type' people ' .

上面的例子中,获取 animal.swim 的时候会报错。

这时我们可以使用类型断言,将 animal 断言成 fish

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

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

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

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

interface fish {
  name: string; 
  swim(): void;
}
function swim(animal: people| fish) { 
  (animal as fish).swim();
}
const mySelf: people = {
  name:'胖奶奶与胖小伙',
  run:() => { 
    console.1og( 'run') 
  }
};
swim(mySelf);

上面的例子编译时不会报错,但在运行时会报错:

//Uncaught TypeError: animal.swim is not a function^

原因是(animal as fish).swim()这段代码隐藏了animal

可能为people 的情况,将animal 直接断言为fish,而TypeScript编译器信任了我们的断言,故在调用swim() 时间没有编译错误。

可是swim 函数接受的参数是people | fish ,但是传入的参数是people类型的变量,由于people上没有 swim 方法,就会导致运行时出现错误了。总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

2、将一个父类断言为更加具体的子类。当类之间有继承关系时,类型断言也是很常见

class ApiError extends Error { 
    code: number = 0 ;
}
class AxisoError extends Error { 
    statusCode: number = 200;
}
const 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

const isApiError = (error: Error) => {
  if (typeof (error instanceof ApiError).code === ' number ' ) {
  	return true;
  }
	return false;
}

上面的例子中,确实使用 instanceof 更加合适,因为 ApiError是一个 JavaScript 的类,能够通过 instanceof 来判断error是否是它的实例。

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

interface ApiError extends Error {
    code: number;
}
interface AxisoError extends Error {
    statusCode: number;
}
const isApiError = (error: Error) => {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}
//error:' 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;
}
const isApiError = (error: Error) => {
    if (typeof (error as ApiError).code === ' number') {
        return true;
    }
    return false;
}

3、将任何一个类型断言为any ts 的类型系统运转良好,每个值类型都具体而精确。当我们引用一个在此类型 上不存在的属性或方法时,就会报错:

const num: number = 1
num.length = 1
// error: Property ' length' does not exist on type ' number '

上面的例子中,数字类型的变量 num 是没有length 属性的,故ts给出了相应的错误提示。

但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:

window.num = 1
// error: Property 'num' does not exist on type 'Window & typeof globalThis'

上面的例子中,我们需要将window.上添加一个属性 num,但ts编译时会报错,提示我们window上不存在nun属性。 此时我们可以使用 as any 临时将 window 断言为 any 类型

(window as any).num = 1;

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

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

我们不能滥用 as any ,也不要否定它的作用

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

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

遇到any类型的变量时,我们可以选择无视它,任由它滋生更多的any。

我们也可以选择改进它,通过类型断言及时把any断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。

例如项目之前定义了一个 getCalander,它的返回值是any:

const getCalander = (key: string): any => {
  return (window as any).cache[key];
}

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

选择语言

const getCalander = (key: string): any =>{
  return (window as any). cache[key];
}
interface CalanderType { 
  date: string;
  callCurrentData(): void;
}
const getCurrentDate = CalanderType( '2022-05-03') as CalanderType;
getCurrentDate.run();

上面的例子中,我们调用完getCalander之后,将它断言为CalanderType类型,从而明确getCurrentDate的类型,后续对getCurrentDate的访问时就有了代码补全,提高承俄码以可维护性。

5、类型断言的限制

根据之前的例子,我们可以得出:

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

2、父类可以被断言为子类

3、任何类型都可以被断言为any

4、 any 可以被断言为任何类型

那么类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?

其实并不是任何一个类型都可以被断言为任何另一个类型。

具体来说,若A兼容B,那么A能够被断言为B,B也能被断言为A

下面我们通过一个简化的例子,来理解类型断言的限制:

interface Animal { 
  name: string;
}
interface People { 
  name: string; 
  run(): void;
}
const Animal1 = (animal: Animal) => { 
  return (animal as People);
}
const isMySelf = (mySelf: People) => { 
  return (mySelf as Animal);
}

上例是可以断言的,我们再看看下面的栗子:

interface Animal {
    name: string;
}
interface People {
    run(): void;
}
const Animal1 = (animal: Animal) => {
    return (animal as People);
}
const isMySelf = (mySelf: People) => {
    return (mySelf as Animal);
}

这个时候会提示错误,两者不能充分重叠,这意味要想断言成功,还必须具备有一个条件:

即:要使得A能够被断言为B,只需要A兼容B或B兼容A即可

6、双重断言(不建议使用) ”任何类型都可以被断言为any , any 可以被断言为任何类型

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

interface People { 
     run(): void;
}
interface Fish { 
    swim(): void;
}
const isPeople = (mySelf: People) => {
    return (mySelf as any as Fish);
}

在上面的例子中,若直接使用mySelf as Fish肯定会报错,因为mySelf 和Fish 互相都不兼容。

若使用双重断言,则可以打破「要使得A能够被断言为B,只需要A兼容B或B兼容A即可」的限制,将任何一个类型断言为任何另一个类型到若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。

二、ts函数的点简单梳理

1、函数声明

在JavaScript中,有两种常见的定义函数的方式函数声明 (Function Declaration)和函数表达式(Function Expression) :

函数声明(Function Declaration)

function sum(x, y) {
    return x + y
}

函数表达式(Function Expression)

const mySun = (x , y):number => {
    return x + y;
}

一个函数有输入和输出,要在TypeScript中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

function sum(x: number, y: number): number{
     return x + y 
};

注意,输入多余的(或者少于要求的)参数,是不被允许的:

function sum(x: number, y: number): number { 
    return x + y;
}
sum(1, 2, 3);
//  error: Supplied parameters do not match any signature of call target .

2、函数,可选参数,参数默认值可选参数

前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?与接口中的可选属性类似,我们用 ? 表示可选的参数:

function myName(firstName: string, lastName?: string) {
  if (lastName) {
	return firstName + ' ' + lastName ;
    } else {
	return firstName;
    }
}
let animal1 = myName('Tom', 'Cat' ); 
let animal2 = myName( 'Tom' );

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:

function myName(firstName?: string, lastName: string) { 
    if (firstName)
	return firstName + ' ' + lastName ;
    } elset
        return las tName;
    }
}
let animal1 = myName('Tom', ' Cat' ); 
let animal2 = myName(undefined, ' Tom' );
//  error: A required parameter cannot follow an optional parameter.

参数默认值 在ES6中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:

function myName(firstName: string, lastName: string = 'Cat') { 
    return firstName + ' ' + lastName ;
}
let animal1 = myName('Tom', 'Cat');
let animal2 = myName( 'Tom ' );

此时就不受「 可选参数必须接在必需参数后面」的限制了:

function myName(firstName: string =‘Tom' ,lastName: string) { 
    return firstName + ' ' + lastName;
}
let animal1 = myName('Tom', 'Cat'); 
let animal2 = myName( undefined, 'Cat' );

3、函数剩余参数、重载 。ES6中,可以使用..rest 的方式获取函数中的剩余参数(rest 参数) :

function pushWay(array, ...items) { 
    items。forEach( function(item) { 
      array . push(item);
    });
}
let a: any[] = [];
pushWay(a, 1, 2, 3);

事实上,items 是一个数组。 所以我们可以用数组的类型来定义它:

function pushWay(array: any[], ...items: any[]) { 
    items.forEach( function(item) {
        array . push(item);
    });
}
let a = [];
pushWay(a, 1, 2, 3);

注意,rest 参数只能是最后一个参数, 关于rest参数,可以参考ES6中的rest参数。

重载

重载允许个函数接受不同数量或类型的参 数时,作出不同的处理。(方法名相同,参数列表、类型不同)

比如,我们需要实现一个函数reverse ,输入数字123的时候,输出反转的数字321 ,输入字符串'ts' 的时候,输出反转的字符串'st'。那么利用联合类型,我们可以这么实现:

function reverse(x: number| string): number | stringvoid {
    if (typeof x === ' number') {
	return Number(x.toString().split('').reverse().join('')); 
    } else if (typeof x === 'string') {
	return x.split('').reverse(). join('');
    }
}

然而这样有一个缺点, 就是不能够精确表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。 这时,我们可以使用重载来定义多个reverse 的函数类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number| string): number | string| void { 
  if (typeof x === ' number') {
	return Number(x.toString().split('').reverse().join('')); 
  } else if (typeof x === 'string') {
	return x.split('').reverse(). join('');
  }
}