Skip to content

TypeScript 的类型断言

简介

虽然有自动类型推断,但是有时难以完全满足我们的需求

TypeScript 提供了“类型断言”这样一种手段,允许断言某个值的类型,编译器直接采用断言给出的类型。

ts
// 两种写法
<Type>value;
value as Type;

上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为开始支持 React 的 JSX 语法,为避免冲突,引入语法二。

ts
// 报错
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 类型

ts
const username = document.getElementById("username");

if (username) {
  (username as HTMLInputElement).value; // 正确
}

应用:指定变量的具体类型。

ts
const value: unknown = "Hello World";

const s1: string = value; // 报错
const s2: string = value as string; // 正确

应用:为联合类型指定具体类型。

ts
const s1: number | string = "hello";
const s2: number = s1 as number;

总的来说,类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误隐患

类型断言的条件

类型断言并不意味着,可以把某个值断言为任意类型,使用前提是值的实际类型与断言的类型必须满足条件exprT的子类型,或者Texpr的子类型。

ts
expr as T;

也就是说,类型断言要求实际类型与断言类型兼容,实际类型可以断言为一个父类型或子类型,但不能断言为一个完全无关的类型。

当然,也可以断言成一个完全无关的类型。通过连续两次断言实现,先断言成 unknown 或 any ,然后再断言为目标类型。

ts
// 或者 <T><unknown>expr
expr as unknown as T;

as const 断言

默认情况下,let 命令声明的变量,会被类型推断为基本类型之一;const 命令声明的变量会被推断为值类型常量

ts
// s1 string
let s1 = "JavaScript";

// s2 “JavaScript”
const s2 = "JavaScript";

有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。

ts
let s = "JavaScript";

type Lang = "JavaScript" | "TypeScript" | "Python";

function setLang(language: Lang) {
  /* ... */
}

setLang(s); // 报错,s的类型被推断为string

TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。

ts
let s = "JavaScript" as const; // s 的类型被推断为 “JavaScript”
setLang(s); // 正确

使用了as const断言以后,let 变量就不能再改变值了。

ts
let s = "JavaScript" as const;
s = "Python"; // 报错

as const断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。

ts
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会将数组变成只读元组,可以用于函数的剩余参数

ts
function add(x: number, y: number) {
  return x + y;
}

const nums = [1, 2];
const total = add(...nums); // 报错

非空断言

对于那些可能为空的变量(即可能等于undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

非空断言在实际编程中很有用,有时可以省去一些额外的判断。

ts
const root = document.getElementById("root")!;

不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。

ts
const root = document.getElementById("root");

if (root === null) {
  throw new Error("Unable to find DOM element #root");
}

root.addEventListener("click", (e) => {
  /* ... */
});

非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。

ts
class Point {
  x!: number; // 正确
  y!: number; // 正确

  constructor() {
    // ...
  }
}

另外,非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefinednull

🔦 断言函数

断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。

为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。

ts
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,其中assertsis都是关键词,value是函数的参数名,string是函数参数的预期类型。

使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。

例子:函数allowsReadAccess()用来断言参数level一定等于rrw

ts
type AccessLevel = "r" | "w" | "rw";

function allowsReadAccess(level: AccessLevel): asserts level is "r" | "rw" {
  if (!level.includes("r")) throw new Error("Read not allowed");
}

如果要将断言函数用于函数表达式,可以采用下面的写法。

ts
// 写法一
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值,

ts
function isString(value: unknown): value is string {
  return typeof value === "string";
}

下面是一个示例,判断一个对象是否有 .next 方法,体现了类型守卫和类型断言的区别。

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