先记住这个答案
TypeScript 会根据可变性和上下文决定是否拓宽字面量类型。const 保存单个字符串时通常能保留具体字面量;const 保存对象时,属性仍可修改,因此字符串属性往往推断为 string。需要保留有限取值,可以给字段明确的字面量联合注解、使用合适的 satisfies 上下文,或对字面量表达式使用 as const。后者还会产生只读属性和只读元组,但这些都是静态描述,不会自动执行 Object.freeze,也不能恢复一个早已被拓宽变量的原始候选集合。
- const 约束绑定,对象属性仍可能可变
- as const 保留字面量并影响只读属性与元组
- 窄类型需要与后续合法更新方式一起设计
为何 const 对象仍然得到宽字段
假设对象初始化时 mode 是 light,但随后切换主题需要把它改为 dark。const 只禁止把变量重新指向另一个对象,没有禁止属性赋值。推断成 string 能容纳这种更新,不过也可能过宽,以至于不能直接传给只接受两种主题的函数。
下面保留了三种意图:普通对象提供可变字符串,常量断言描述固定配置,显式联合注解则描述可以在合法主题之间切换的状态。排错时先确定对象到底是常量配置还是可变状态,再选择类型写法,不应一律追加断言。
type Theme = 'light' | 'dark';
const name = 'light';
const loose = { mode: 'light' };
// @ts-expect-error loose.mode 已推断为 string
const tooWide: Theme = loose.mode;
const fixed = { mode: 'light' } as const;
const exact: 'light' = fixed.mode;
const state: { mode: Theme } = { mode: 'light' };
state.mode = 'dark';
// @ts-expect-error 状态只允许声明的两种主题
state.mode = 'sepia';普通对象字段不能直接作为有限主题集合使用,固定配置保留 light 字面量,可变状态接受 dark 却拒绝其他字符串。示例通过预期错误验证边界,而不是声称所有 const 都只读。
数组和传参也受上下文影响
普通数组字面量常被推断成可以增删的数组,因此元素类型会合并、拓宽,长度也不固定。as const 可以把直接写出的数组保留为只读元组,适合路由名称等静态列表;如果接收方要求可变数组,则需要调整接收契约或创建可变副本。
satisfies 在检查兼容性的同时保留表达式自身信息,但提供的上下文可能使某些字段保持字面量。因此它不是先推断完再做一个绝无影响的检查。遇到不同推断结果,应查看具体字段类型和后续赋值需求,不要只记住保留类型这几个字。
常量断言不会替代运行时不可变
as const 不会在生成的 JavaScript 中插入冻结代码。通过其他可写引用仍可能改变共享对象,而对字面量中引用的已有数组也不能简单理解为自动深度冻结。若业务需要运行时不可变保证,应另外设计复制、冻结或不可变更新策略。
已经从可变来源得到 string 的变量,再包进对象并使用常量断言,也不能凭空知道它只可能来自两个主题。保留信息要发生在声明边界,或通过运行时检查证明属于允许集合。把任意字符串断言成主题只是转移责任,并没有检查真实值。
容易答错的地方
- const 对象的所有字段都是字面量只读
- const 限制变量重新绑定,对象属性通常仍可写;字段是否保留字面量还受到注解、上下文和常量断言影响。
- as const 会在浏览器里深度冻结对象
- 常量断言在编译后被擦除,不执行冻结操作;共享引用的实际可变性需要通过运行时代码和所有权约定处理。
面试官还会怎么问?
可变枚举状态适合直接加 as const 吗?
通常更适合显式声明允许值的联合类型,否则初始字段可能只允许当前字面量,后续合法状态切换也会被编译器拒绝。
怎样从固定列表得到允许值类型?
可将字面量列表声明为只读元组,再使用 typeof 列表[number] 提取元素联合;外部输入仍需检查是否属于这个列表。
函数返回对象的字段为什么也可能变宽?
返回对象如果没有窄的上下文或明确注解,仍可能按可变属性推断;应在返回契约或对象构造处保留需要的有限取值信息。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。