类型断言
1741字约6分钟
TypeScript
2023-01-30
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
类型断言有两种语法:
// 语法一:<类型>值
<Type>value;
// 语法二:值 as 类型
value as Type;
上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
下面是一个网页编程的实际例子。
const username = document.getElementById("username");
if (username) {
(username as HTMLInputElement).value; // 正确
}
上面示例中,变量username
的类型是HTMLElement | null
,排除了null
的情况以后,HTMLElement
类型是没有value
属性的。如果username
是一个输入框,那么就可以通过类型断言,将它的类型改成HTMLInputElement
,就可以读取value
属性。
注意,上例的类型断言的圆括号是必需的,否则username
会被断言成HTMLInputElement.value
,从而报错。
注意
类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
类型断言的其他用处:
- 指定 unknown 类型的变量的具体类型
const value: unknown = "Hello World";
const s1: string = value; // 报错
const s2: string = value as string; // 正确
- 指定联合类型的值的具体类型
const s1: number | string = "hello";
const s2: number = s1 as number;
✨类型断言的条件
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件:
类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。
✨as const 断言
如果没有声明变量类型,let
命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const
命令声明的变量,则被推断为值类型常量。
// 类型推断为基本类型 string
let s1 = "JavaScript";
// 类型推断为字符串 “JavaScript”
const s2 = "JavaScript";
as const
的限制:
使用了
as const
断言以后,let 变量就不能再改变值了。as const
断言只能用于字面量,不能用于变量。as const
也不能用于表达式。
as const
的用处:
as const
断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。由于
as const
会将数组变成只读元组,所以很适合用于函数的 rest 参数
✨非空断言
对于那些可能为空的变量(即可能等于undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
。
function f(x?: number | null) {
validateNumber(x); // 自定义函数,确保 x 是数值
console.log(x!.toFixed());
}
function validateNumber(e?: number | null) {
if (typeof e !== "number") throw new Error("Not a number");
}
不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。
const root = document.getElementById("root");
if (root === null) {
throw new Error("Unable to find DOM element #root");
}
root.addEventListener("click", (e) => {
/* ... */
});
另外,非空断言只有在打开编译选项strictNullChecks
时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined
或null
。
✨断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
// 断言函数
function isString(value: unknown): void {
if (typeof value !== "string") throw new Error("Not a string");
}
const aValue: string | number = "Hello";
isString(aValue);
为了更清晰地表达断言函数,TypeScript 3.7
引入了新的类型写法。
function isString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
上面示例中,函数isString()
的返回值类型写成asserts value is string
,其中asserts
和is
都是关键词,value
是函数的参数名,string
是函数参数的预期类型。它的意思是,该函数用来断言参数value
的类型是string
,如果达不到要求,程序就会在这里中断。
使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。
注意,函数返回值的断言写法,只是用来更清晰地表达函数意图,真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错。
另外,断言函数的asserts
语句等同于void
类型,所以如果返回除了undefined
和null
以外的值,都会报错。
如果要断言参数非空,可以使用工具类型NonNullable<T>
。
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(`${value} is not defined`);
}
}
如果要将断言函数用于函数表达式,可以采用下面的写法:
// 写法一
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");
};
注意,断言函数与类型保护函数(type guard)是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值。
// 类型保护函数
function isString(value: unknown): value is string {
return typeof value === "string";
}
如果要断言某个参数保证为真(即不等于false、undefined和null),TypeScript 提供了断言函数的一种简写形式:
function assert(x: unknown): asserts x {
if (!x) {
throw new Error(`${x} should be a truthy value.`);
}
}
这种断言函数的简写形式,通常用来检查某个操作是否成功。
type Person = {
name: string;
email?: string;
};
function loadPerson(): Person | null {
return null;
}
let person = loadPerson();
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
// Error: Person is not defined
assert(person, "Person is not defined");
console.log(person.name);