表达式后面的感叹号 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!.name | if (user) { user.name } | 跳过这个代码块 |
user!.name | user?.name | undefined |
count! | count ?? 0 | 使用默认值 |
map.get(k)! | must(map.get(k), "k") | 在这一行抛出清晰的错误 |
let x!: T | const 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),或者一个会抛出清晰错误的辅助函数。只在值由编译器无法跟踪的逻辑保证存在的地方使用 !。