TypeScript 的类型断言
简介
虽然有自动类型推断,但是有时难以完全满足我们的需求
TypeScript 提供了“类型断言”这样一种手段,允许断言某个值的类型,编译器直接采用断言给出的类型。
// 两种写法
<Type>value;
value as Type;上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为开始支持 React 的 JSX 语法,为避免冲突,引入语法二。
// 报错
const p: { x: number } = { x: 0, y: 0 };
// 因为直接对象赋值,默认的是这样一个字面量类型
type T = {
x: 0,
y: 0
}
const p: T = { 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 };应用:断言 dom 类型
const username = document.getElementById("username");
if (username) {
(username as HTMLInputElement).value; // 正确
}应用:指定变量的具体类型。
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;总的来说,类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误隐患
类型断言的条件
类型断言并不意味着,可以把某个值断言为任意类型,使用前提是值的实际类型与断言的类型必须满足条件 ,expr是T的子类型,或者T是expr的子类型。
expr as T;也就是说,类型断言要求实际类型与断言类型兼容,实际类型可以断言为一个父类型或子类型,但不能断言为一个完全无关的类型。
当然,也可以断言成一个完全无关的类型。通过连续两次断言实现,先断言成 unknown 或 any ,然后再断言为目标类型。
// 或者 <T><unknown>expr
expr as unknown as T;as const 断言
默认情况下,let 命令声明的变量,会被类型推断为基本类型之一;const 命令声明的变量会被推断为值类型常量。
// s1 string
let s1 = "JavaScript";
// s2 “JavaScript”
const s2 = "JavaScript";有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。
let s = "JavaScript";
type Lang = "JavaScript" | "TypeScript" | "Python";
function setLang(language: Lang) {
/* ... */
}
setLang(s); // 报错,s的类型被推断为stringTypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。
let s = "JavaScript" as const; // s 的类型被推断为 “JavaScript”
setLang(s); // 正确使用了as const断言以后,let 变量就不能再改变值了。
let s = "JavaScript" as const;
s = "Python"; // 报错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; }总之,as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。
由于as const会将数组变成只读元组,可以用于函数的剩余参数
function add(x: number, y: number) {
return x + y;
}
const nums = [1, 2];
const total = add(...nums); // 报错非空断言
对于那些可能为空的变量(即可能等于undefined或null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!。
非空断言在实际编程中很有用,有时可以省去一些额外的判断。
const root = document.getElementById("root")!;不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。
const root = document.getElementById("root");
if (root === null) {
throw new Error("Unable to find DOM element #root");
}
root.addEventListener("click", (e) => {
/* ... */
});非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。
class Point {
x!: number; // 正确
y!: number; // 正确
constructor() {
// ...
}
}另外,非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined或null。
🔦 断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。
function isString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
let x: any = 1997 // x 的类型为任意变量
isString(x)
x.slice(1) // x 的类型为 string上面示例中,函数isString()的返回值类型写成asserts value is string,其中asserts和is都是关键词,value是函数的参数名,string是函数参数的预期类型。
使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。
例子:函数allowsReadAccess()用来断言参数level一定等于r或rw。
type AccessLevel = "r" | "w" | "rw";
function allowsReadAccess(level: AccessLevel): asserts level is "r" | "rw" {
if (!level.includes("r")) throw new Error("Read not allowed");
}如果要将断言函数用于函数表达式,可以采用下面的写法。
// 写法一
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)是两种不同的函数。断言函数不返回值,而类型保护函数总是返回一个布尔值。
下面的 isString 是一个类型守卫函数,检查参数value是否为字符串并返回bool值,
function isString(value: unknown): value is string {
return typeof value === "string";
}下面是一个示例,判断一个对象是否有 .next 方法,体现了类型守卫和类型断言的区别。
interface NextLike{
next: (...args:any[])=>any
}
// 类型守卫
function hasNext(x:unknown): x is NextLike {
return typeof x === 'object' &&
x !== null &&
'next' in x && // 有 'next' 属性
typeof x.next === 'function'
}
// 用法:
let param: any = '123' // 某个变量
if(hasNext(param)){
param.next() // 不报错
}
// 类型断言
function assertHasNext(x:unknown): asserts x is NextLike {
const ans = typeof x === 'object' &&
x !== null &&
'next' in x && // 有 'next' 属性
typeof x.next === 'function'
if(!ans) throw new Error("don`t have next func")
}
let pp: any = '123' // 某个变量
assertHasNext(pp)
pp.next()