Menu

TypeScript 感叹号:非空断言运算符(!)

值后面的感叹号(例如 user!)是非空断言运算符:它从类型中去掉 null 和 undefined,却不做任何运行时检查。本文讲解 x! 的作用、明确赋值断言 let x!: T 和 prop!: T、它们为什么有风险,以及更安全的替代写法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

表达式后面的感叹号 value! 是非空断言运算符。它从类型中去掉 null 和 undefined,所以 number | undefined 可以当作 number 使用。这是对编译器的一个承诺,而不是一次检查:运行时什么也不会发生。

没有 ! 时,tea.toFixed(2) 会报错 TS18048:'tea' is possibly 'undefined'。加上它之后,代码能编译通过,因为你告诉了编译器这个键存在。

x! 在运行时做什么:什么都不做

! 会从输出中擦除。prices.get("coffee")! 编译出的 JavaScript 就是 prices.get("coffee")。如果断言是错的,错误会在之后出现,出现在第一个使用这个缺失值的地方:

程序打印 Cannot read properties of undefined (reading 'toFixed')。崩溃发生在 ! 的下一行,而在真实代码中可能远得多:undefined 可能被存进某个对象,然后在另一个文件里出错。正是这段距离让 ! 变得有风险。

明确赋值:let x!: T

同一个符号出现在声明中时,含义与之相关。TypeScript 会跟踪变量是否在读取之前被赋值,但它无法跟踪在另一个函数里完成的赋值:

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number }; 是明确赋值断言,意思是“这个变量会在任何读取之前被赋值”。它能消除错误,但和 x! 有同样的隐患:如果某次跳过了 init(),读取到的就是 undefined。通常更好的做法是调整代码结构,例如让 init 返回这个对象,然后写 const config = init();。

类属性:prop!: T

开启 strictPropertyInitialization(属于 strict 的一部分)后,每个类属性都必须在声明中或构造函数中初始化。否则会得到 TS2564:Property 'socket' has no initializer and is not definitely assigned in the constructor.。当属性稍后由某个方法或框架设置时,prop!: T 让编译器接受它:

第一个 console.log 暴露了其中的空隙:类型说 socket 总是存在,但在 open() 之前它是 undefined。此时调用 c.socket.send 能编译通过,却会抛出异常。如果这个属性确实可能缺失,就声明为 socket?: ... 并检查它,或者在构造函数中创建这个对象。prop! 成为标准做法的主要场景,是在构造之后才填充属性的框架:Angular 的 @ViewChild(...) child!: ChildDirective(在 ngAfterViewInit 运行之前被设置),或者 ORM 的实体类,库在加载一行数据时填充它的各个列(MikroORM 的文档写的是 @Property() title!: string)。

更安全的替代写法

大多数 ! 都可以换成编译器会检查的写法,或者换成在正确位置大声报错的检查:

这两个辅助函数都像 ! 一样收窄类型,但错误的假设会当场产生 missing HOST,而不是在别处产生 TypeError。assertDefined 是一个断言函数(asserts value is ...):调用之后,编译器把 host 当作 string。更多模式见 类型守卫。

不要写改写为值缺失时会发生什么
user!.nameif (user) { user.name }跳过这个代码块
user!.nameuser?.nameundefined
count!count ?? 0使用默认值
map.get(k)!must(map.get(k), "k")在这一行抛出清晰的错误
let x!: Tconst x = compute()没有出错的可能

其他的感叹号

! 的含义取决于它所在的位置:

代码含义
value!(在表达式之后)非空断言,仅限 TypeScript
let x!: T、prop!: T明确赋值断言,仅限 TypeScript
!value(在表达式之前)逻辑非,普通 JavaScript
!!value转换为布尔值,普通 JavaScript
a !== b、a != b不等,普通 JavaScript

只有前两种会在编译时被擦除。!value 和 !!value 在运行时执行,并返回一个布尔值。

常见问题

TypeScript 中变量后面的感叹号是什么意思?

value! 是非空断言运算符。它告诉编译器 value 不是 null 或 undefined,于是它的类型去掉了这两个成员:string | undefined 变成 string。它会从生成的 JavaScript 中删除,不会添加任何运行时检查,所以如果你判断错了,程序会在之后因为 TypeError 而出错。

TypeScript 中的 ! 和 ? 有什么区别?

x! 断言值存在,并给你非空的类型,不做任何检查。x?.y 在运行时检查:如果 x 是 null 或 undefined,就停下来并返回 undefined。在声明中,name?: string 让属性变为可选,而 name!: string 表示这个必需的属性会在编译器看不到的地方被赋值。

let x!: string 是什么意思?

这是明确赋值断言。它告诉编译器这个变量会在被读取之前赋值,即使编译器无法证明这一点(例如赋值发生在另一个函数里)。没有它,读取这个变量会报错 TS2454:Variable 'x' is used before being assigned。

如何修复“has no initializer and is not definitely assigned in the constructor”?

这是 strictPropertyInitialization 报出的 TS2564 错误。给属性一个初始值、在构造函数中赋值、把它变为可选(prop?: T),或者在框架或初始化方法确实会在使用前设置它时,写成 prop!: T。! 是最后的选择,因为没有任何东西会检查这个承诺。

使用非空断言运算符是不好的做法吗?

它本身没有错,但每个 ! 都是一个未经检查的声明。@typescript-eslint/no-non-null-assertion 这样的 lint 规则会标记它。应优先使用能收窄类型的检查(if (x)、x ?? fallback、x?.y),或者一个会抛出清晰错误的辅助函数。只在值由编译器无法跟踪的逻辑保证存在的地方使用 !。

Coddy 编程语言插图

和 Coddy 一起学编程

开始