类型断言和类型守卫
类型断言
TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。
类型断言的一个常见用例是当你从 JavaScript 迁移到 TypeScript 时:
const foo = {};
foo.bar = 123; // Error 类型“{}”上不存在属性“bar”。
foo.baz = 'hello'; // Error 类型“{}”上不存在属性“baz”。
这里的代码发出了错误警告,因为 foo 的类型推断为 {},即没有属性的对象。因此,你不能在它的属性上添加 bar 或 baz,你可以通过类型断言来避免此问题:
interface Foo {
bar: number;
baz: string;
}
const foo = {} as Foo;
foo.bar = 123;
foo.baz = 'hello';
类型断言与类型转换
它之所以不被称为「类型转换」,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法
类型断言被认为是有害的
千万不要滥用类型断言,类型断言会造成 TypeScript 丧失代码提示的能力
interface Foo {
bar: number;
baz: string;
}
const foo = {} as Foo;
foo.bar = 1;
可以看到虽然没有添加 baz 属性,但是 IDE 并没有提醒我们,其实上述情况正确的做法是这样的:
interface Foo {
bar: number;
baz: string;
}
const foo: Foo = {};
// ERROR 类型“{}”缺少类型“Foo”中的以下属性: bar, baz
此时 IDE 会马上提向我们遗漏了属性 bar 和 baz.
双重断言
虽然类型断言是有强制性的,但并不是万能的,因为一些情况下也会失效:
interface Person {
name: string;
age: number;
}
const person = 'wyh' as Person;
// Error 类型 "string" 到类型 "Person" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
很显然不能把 string 强制断言为一个接口 Person,但是并非没有办法,此时可以使用双重断言:
interface Person {
name: string;
age: number;
}
const person = 'wyh' as unknown as Person;
先把类型断言为 unknown 或者 any ,再断言为其他类型就能实现双重断言,记住双重断言也千万不要滥用。
非空断言
在上下文中当类型检查器无法断定类型时,后缀表达式操作符 !,可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined。
下面我们先来看一下非空断言操作符的一些使用场景。
- 忽略
undefined和null类型
function myFunc(maybeString: string | undefined | null) {
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!;
}
// 不能将类型“string | null | undefined”分配给类型“string”。
// 不能将类型“undefined”分配给类型“string”。
- 调用函数时忽略
undefined类型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
const num1 = numGenerator(); // Error
const num2 = numGenerator!();
}
// 对象可能为“未定义”。
// 不能调用可能是“未定义”的对象。
因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。
比如下面这个例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
编译后:
"use strict";
var a = undefined;
var b = a;
console.log(b);
虽然在 TS 代码中,我们使用了非空断言使得 const b: number = a!,这样语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined。
类型守卫
类型守卫说白了就是缩小类型的范围,允许你使用更小范围下的对象类型。
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。
in
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
typeof
TypeScript 熟知 JavaScript 中 instanceof 和 typeof 运算符的用法。如果你在一个条件块中使用这些,TypeScript 将会推导出在条件块中的变量类型。
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename
"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,只是语言不会把那些表达式识别为类型保护。
instanceof
这有一个关于 class 和 instanceof 的例子:
class Foo {
foo = 123;
common = '123';
}
class Bar {
bar = 123;
common = '123';
}
function doStuff(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo);
console.log(arg.bar); // Error
}
if (arg instanceof Bar) {
console.log(arg.foo); // Error
console.log(arg.bar);
}
}
doStuff(new Foo());
doStuff(new Bar());
TypeScript 甚至能够理解 else。当你使用 if 来缩小类型时,TypeScript 知道在其他块中的类型并不是 if 中的类型
字面量类型保护
当你在联合类型里使用字面量类型时,你可以检查它们是否有区别:
type Foo = {
kind: 'foo'; // 字面量类型
foo: number;
};
type Bar = {
kind: 'bar'; // 字面量类型
bar: number;
};
function doStuff(arg: Foo | Bar) {
if (arg.kind === 'foo') {
console.log(arg.foo); // ok
console.log(arg.bar); // Error
} else {
// 一定是 Bar
console.log(arg.foo); // Error
console.log(arg.bar); // ok
}
}
使用定义的类型保护
JavaScript 并没有内置非常丰富的、运行时的自我检查机制。当你在使用普通的 JavaScript 对象时(使用结构类型,更有益处),你甚至无法访问 instanceof 和 typeof。在这种情景下,你可以创建用户自定义的类型保护函数,这仅仅是一个返回值为类似于someArgumentName is SomeType 的函数,如下:
// 仅仅是一个 interface
interface Foo {
foo: number;
common: string;
}
interface Bar {
bar: number;
common: string;
}
// 用户自己定义的类型保护!
function isFoo(arg: Foo | Bar): arg is Foo {
return (arg as Foo).foo !== undefined;
}
// 用户自己定义的类型保护使用用例:
function doStuff(arg: Foo | Bar) {
if (isFoo(arg)) {
console.log(arg.foo); // ok
console.log(arg.bar); // Error
} else {
console.log(arg.foo); // Error
console.log(arg.bar); // ok
}
}
doStuff({ foo: 123, common: '123' });
doStuff({ bar: 123, common: '123' });