TypeScript 类型断言
1. 简介
对于没有类型声明的值,TS 会进行类型推断,很多时候得到的结果,未必是开发者想要的。
type T = "a" | "b" | "c";
let foo = "a";
let bar: T = foo; // 报错
2
3
4
上面示例中,最后一行报错,原因是 TS 推断变量 foo
的类型是 string
,而变量 bar
的类型是 'a'|'b'|'c'
,前者是后者的父类型,所以就报错了。
TS 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型。TS 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。这样虽然削弱了 TS 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码:
type T = "a" | "b" | "c";
let foo = "a";
let bar: T = foo as T; // 正确
2
3
4
类型断言有两种语法:
- 语法一
<Type>value
:let bar = <T>foo;
- 语法二
value as Type
:let bar = foo as T;
两种语法是等价的,早期只有语法一,后来因为需要支持 JSX
语法,引入了语法二。目前使用语法一时必须关闭 TS 的 JSX 支持,推荐使用语法二。
类型断言可以绕过 TS 的类型推断,所以也可以“欺骗”TS 解决类型冲突的问题:
// 报错
// 右侧会推断为一个字面量类型,不遵循“结构原则”
const p: { x: number } = { x: 0, y: 0 };
// 正确
const p0: { x: number } = { x: 0, y: 0 } as { x: number };
// 正确
// 断言的类型(不是字面量类型)为左边类型的子类型,可以赋值
const p1: { x: number } = { x: 0, y: 0 } as { x: number; y: number };
2
3
4
5
6
7
8
9
10
对于变量后存在连续的 JS 语法代码时(例如属性取值或函数调用),需要将类型断言用括号括起来: (a as any).value = 1
否则会报错。类型断言不应滥用,因为它改变了 TS 的类型检查,很可能埋下错误的隐患。类型断言的一大用处是,指定 unknown
类型的变量的具体类型:
const value: unknown = "Hello World";
const s1: string = value; // 报错
const s2: string = value as string; // 正确
2
3
4
另外,类型断言也适合指定联合类型的值的具体类型:
const s1: number | string = "hello";
const s2: number = s1 as number;
2
2. 类型断言的条件
类型断言并不意味着,可以把某个值断言为任意类型。值的实际类型与断言的类型必须满足实际类型是断言类型的子类型,或者断言类型是实际类型的子类型:
const n = 1;
const m: string = n as string; // 报错
2
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为 any 类型和 unknown 类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介:
const n = 1;
const m: string = n as any as string; // 不报错
2
3. as const 断言
如果没有声明变量类型,let
命令声明的变量,会被类型推断为 TS 内置的基本类型之一;const
命令声明的变量,则被推断为值类型常量:
// 类型推断为基本类型 string
let s1 = "JavaScript";
// 类型推断为字符串 “JavaScript”
const s2 = "JavaScript";
2
3
4
5
TS 提供了一种特殊的类型断言 as const
,用于告诉编译器推断类型时,可以将这个值推断为常量。但是使用 as const 后,即使是用 let
声明的变量也无法再改变值,相当于变成了 const
声明(注意,as const 断言只能用于字面量,不能用于变量或表达式):
let s1 = "JavaScript" as const;
s1 = "Python"; // 报错
let s2 = "JavaScript";
foo(s2 as const); // 报错
let s = ("Java" + "Script") as const; // 报错
2
3
4
5
6
as const 断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的:
const v1 = {
x: 1,
y: 2,
}; // 类型是 { x: number; y: number; }
const v2 = {
x: 1 as const,
y: 2,
}; // 类型是 { x: 1; y: number; }
const v3 = {
x: 1,
y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }
2
3
4
5
6
7
8
9
10
11
12
13
14
总之,as const
会将字面量的类型断言为不可变类型,缩小成 TS 允许的最小类型。使用 const 声明数组时会推断为type[]
,使用 as const 断言后则会推断为元组类型。此外 Enum 成员也可以使用 as const 断言:
enum Foo {
X,
Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X
2
3
4
5
6
上面示例中,如果不使用 as const 断言,变量 e1 的类型被推断为整个 Enum 类型;使用了 as const 断言以后,变量 e2 的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
4. 非空断言
对于那些可能为空的变量(即可能等于 undefined
或 null
)TS 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
。非空断言在实际编程中很有用,有时可以省去一些额外的判断:
const root = document.getElementById("root");
root.addEventListener("click", (e) => {}); // 报错
const root = document.getElementById("root")!;
root.addEventListener("click", (e) => {}); // 正确
// 或者
const root = document.getElementById("root");
root!.addEventListener("click", (e) => {}); // 正确
2
3
4
5
6
7
8
**不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。**另外,非空断言只有在打开编译选项 strictNullChecks
时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为 undefined 或 null。
5. 断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行:
function isString(value: unknown): void {
if (typeof value !== "string") throw new Error("Not a string");
}
function toUpper(x: string | number) {
isString(x);
// 调用后,后续代码 TS 就能确定 x 是字符串
return x.toUpperCase();
}
2
3
4
5
6
7
8
9
传统的断言函数 isString()
的写法有一个缺点,它的参数类型是 unknown
,返回值类型是 void
(即没有返回值)。单单从这样的类型声明,很难看出 isString()
是一个断言函数。为了更清晰地表达断言函数,TS 3.7 引入了新的类型写法:
function isString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
2
3
WARNING
注意,断言函数写法只是用来更清晰地表达函数意图,真正的检查需要开发者自己部署,如果内部的检查与断言不一致,TS 也不会报错:
// 不会报错
function isString(value: unknown): asserts value is string {
if (typeof value !== "number") throw new Error("Not a number");
}
2
3
4
另外,断言函数的 asserts 语句等同于 void 类型,所以如果返回除了 undefined 和 null 以外的值,都会报错。
如果要将断言函数用于函数表达式,可以有两种写法:
// 写法一
const assertIsNumber = (value: unknown): asserts value is number => {
if (typeof value !== "number") throw Error("Not a number");
};
// 写法二
type AssertIsNumber = (value: unknown) => asserts value is number;
const assertIsNumber: AssertIsNumber = (value) => {
if (typeof value !== "number") throw Error("Not a number");
};
2
3
4
5
6
7
8
9
10
11
如果要断言某个参数保证为真(即不等于 false、undefined 和 null),TS 提供了断言函数的一种简写形式:
function assert(x: unknown): asserts x {
if (!x) {
throw new Error(`${x} should be a truthy value.`);
}
}
2
3
4
5
6. 类型谓词(类型保护函数)
类型保护函数与断言函数类似,区别在于,断言函数不返回值,而类型保护函数总是返回一个布尔值。
类型保护函数使用 is
关键字描述函数返回值与参数的关系。当函数返回 true 时,表示参数是描述的类型,否则不是:
function isCat(a: any): a is Cat {
return a.name === "kitty";
}
let x: Cat | Dog;
if (isCat(x)) {
x.meow(); // 正确,因为 x 肯定是 Cat 类型
}
2
3
4
5
6
7
8
9
is 运算符还有一种特殊用法,就是用在类(class
)的内部,描述类的方法的返回值:
class Teacher {
isStudent(): this is Student {
return false;
}
}
class Student {
isStudent(): this is Student {
return true;
}
}
2
3
4
5
6
7
8
9
10
11