TypeScript(八)

117 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

😊 大家好,我是思淼MJ。

上篇文章总结了 TypeScript 的函数类型,在之前的文章中,多次提到了“类型推断”这个词,今天来详细的了解下。

类型断言

说到类型推断,就会想到另外一个名词:类型断言。

类型断言是指可以手动来指定一个值的类型。

语法

关于语法,有两种表达方式:

as 类型

或者

<类型>

建议使用第一种,因为在 tsx 语法(说到tsx 会想到什么?是的没错,React 的 jsx 语法的 ts 版)中必须使用前者;就好比是 `` 语法在 tsx 中表示的是一个 ReactNode,在 ts 中出了表示类型推断以外,还可以用来表示一种 泛型。

用途

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

之前的联合类型的文章中有提到过,当 TypeScript 不确定一个联合类型的变量到底是属于哪种类型的时候,就只能访问此联合类型中的所有类型共有的属性和方法。

举个简单的例子🌰:

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

fonction getName (obj: Cat | Fish) {
    return obj.name;
}

上面例子中,访问了两个接口类型的公共属性,如果在不确定类型的情况下,去访问其中一个类型的特有的属性和方法呢???

比如对上面的例子进行调整:

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

fonction getName (obj: Cat | Fish) {
    if (typeof obj.run === 'fonction') {
        return true;
    }
    return obj.name;
}

执行上面例子,会发现代码报错了:

//   error TS2339: Property 'run' does not exist on type 'Cat | Fish'.
//   Property 'run' does not exist on type 'Cat'.

此时的解决方法是,可以使用类型断言来将 obj 断言为 Cat,如下:

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

fonction getName (obj: Cat | Fish) {
    if (typeof (obj as Cat).run === 'fonction') {
        return true;
    }
    return obj.name;
}

这样就可以解决上面的报错了。

需要注意的点是:简单来说,类型断言只能欺骗 TypeScript 编译器,无法避免运行时的报错,如果滥用断言,可能会造成运行时的报错。比如下面的这个例子🌰:

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

fonction getName (obj: Cat | Fish) {
    return (obj as Cat).run();};

const Tom:Fish = {
    name: 'Tom',
    call() { console.log('abc') }
}

getName(Tom);

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

// Uncaught TypeError:obj.run is not a function

原因:(obj as Cat).run(); 这部分代码屏蔽了 obj 可能是 Fish 的可能性,将 obj 直接断言成了 Cat,同时 TypeScript 编译器信任了我们的断言,所以后面在调用 run() 的时候,没有编译错误。

而后面调用 getName 函数的时候,传入参数是 Fish 类型,但 Fish上面 并没有 run() ,所以会出现运行时报错。

总结:在使用断言的时候,避免断言后调用方法或者是引用深层属性,以减少不必要的运行时错误。

  • 将父类断言为更加具体的子类:

当类之间有继承关系时:

class ApiCallback extends Back {
    mes:string = '请求成功'
};
class ActionCallback extends Back {
    actionMes:string = '操作成功'
};

fonction isApiCallback (message: Back) {
    if (typeof (message as ApiCallback).mes === 'string') {
        return true;
    } else {
        return false;
    }
}

从上面例子中,我们可以得出,声明了一个函数 isApiCallback 来判断传递的参数 msg 是不是 ApiCallback 类型,并将参数 message 的类型与 ApiCallback 继承的类型保持一致,由于 Back 中没有 mes 属性,所以将 message 直接断言成 ApiCallback,这样就在获取 mes 属性的时候就不会报错了。

其实,这个例子中有一个更合适的用来判断的关键字 instanceof :

class ApiCallback extends Back {
    mes:string = '请求成功'
};
class ActionCallback extends Back {
    actionMes:string = '操作成功'
};

fonction isApiCallback (message: Back) {
    if (message instanceof ApiCallback) {
        return true;    } else {
        return false;
    }
}

🤔问题:为什么说使用 instanceof 更合适呢???

📖答案:其实上面例子中的 ApiCallback 是一个类,而 message 相当于是它的一个实例,所以通过 instanceof 来判断会更加合适。

🤔问题:那如果 ApiCallback 不是一个类呢,定义的时候使用的是接口呢???

比如下面的例子:

interface ApiCallback extends Back {
    mes:string = '请求成功'
};
interface ActionCallback extends Back {
    actionMes:string = '操作成功'
};

fonction isApiCallback (message: Back) {
    if (message instanceof ApiCallback) {
        return true;
    } else {
        return false;
    }
}

执行上面的例子,你会发现报错了:

error TS2693: 'ApiCallback' only refers to a type, but is being used as a value here.

是因为,这个时候的 ApiCallback 不再是一个类,是一个接口类型,不是一个真正的值,在编译结果中会被删除掉,所以不能使用 instanceof 来做运行时的判断,所以会报错。

这个时候只能使用类型断言来实现,通过判断参数的某个属性是否存在,进而判断传入参数是否是 ApiCallback。上面的例子可以调整为:

interface ApiCallback extends Back {
    mes:string = '请求成功'
};
interface ActionCallback extends Back {
    actionMes:string = '操作成功'
};

fonction isApiCallback (message: Back) {
    if (typeof (message as ApiCallback).mes=== 'string') {
        return true;
    } else {
        return false;
    }
}
  • 将任何一个类型断言为 any

当我们引用一个此类型上不存在的属性或方法时,比如下面这个例子🌰:

const num: number = 1;
num.length = 1;

执行上面的例子,你会发现报错了:

//  error TS2339: Property 'length' does not exist on type 'number'.

因为上面的例子中,通过报错信息,我们可以得知,num 是个数值类型,而此类型上并没有 length 属性,所以会出现报错。

再来看些另外一个例子🌰:

window.name = 'abc';

从这段代码中,看着很正确,并没有什么错误的部分,但当我们执行这段代码,会发现报错了:

//  error TS2339: Property 'name' does not exist on type 'Window & typeof globalThis'.

从上面的例子以及报错信息中,我们可以得知:在给 winodw 上面添加属性 name 的时候,提示我们  winodw 上面不存在 name 属性。

这个时候,我们可以使用 as any,将 window 直接断言 为 any 类型:

(window as any).name = 'abc';

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

需要的点是:不建议“万物 皆 any”,它有的时候可能会覆盖了真正的类型错误,所以如果不是在特别的确定情况下,不建议使用 as any

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

在日常开发中,不可避免的会出现需要处理 any 类型的情况:有可能是因为类型不够明确,或者是历史遗留的问题,或者是别人写的不太规范的代码。

举个简单的例子🌰:

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

从上面的代码可以看出,返回值的类型为 any 类型,但当我们使用的时候,最好在调用这个函数能够将返回值断言成一个精确的类型,比如可以在使用的时候,可以这样写:

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

这样,后续在悬停 tom 的时候,就会自动有代码类型补全,更加明确了 tom 的类型,提高了代码的可维护性。