const T 只在字面量实参上生效,传变量立刻失效;它把 readonly 一路传染到返回值,内部要改入参直接报错。版本差异和业务坑点讲不清,不算懂 TS 5.x。
面试官问的,不是"它锁字面量"
"TS 里 const 类型参数是干嘛的?"
多数人张口就是:把泛型参数标成 const,让字面量不要自动脱宽。没错。但这是第一层。
面试官想听后面三层:它和 as const 差在哪、什么场景不能无脑加、TS 5.0 为什么非要新造语法,而不是增强泛型推导。同一句官方文档,有人背下来,有人能讲清副作用和编译器动机。
下面按四层拆。
const T 改的是推导,不是值
TS 5.0 之前,泛型参数遇到数组字面量,默认脱宽成元素联合的数组类型。
ts
function loose<T extends readonly unknown[]>(arr: T): T {
return arr
}
const t2 = loose([1, 'a', true])
// 推导结果:(string | number | boolean)[]
加上 const 修饰,同一个调用点会保留字面量:
ts
function tuple<const T extends readonly unknown[]>(arr: T): T {
return arr
}
const t1 = tuple([1, 'a', true])
// readonly [1, "a", true]
type Item = (typeof t1)[number]
// 1 | "a" | true
关键点:const 类型参数作用在"推导",不在"值"。它告诉编译器:这个参数位置上别按默认规则脱宽,尽量保留字面量。
那结果为什么自动带 readonly?
要在类型层面锁死一个字面量,就必须禁止通过可变引用把它改宽。TS 复用 as const 的那套语义:字面量收窄 + 深层 readonly。所以 const T 的产物天然是不可变元组,不是"更窄"的数组。
as const 和 const T:别混为一谈
三年经验的人最容易在这里含糊。两者都能锁字面量,但作用位置完全不同。
| 维度 | as const | const 类型参数 |
|---|---|---|
| 作用对象 | 值表达式 | 泛型声明 |
| 控制权 | 每个调用点手写 | 声明处写一次 |
| 传变量时 | 失效(必须字面量表达式) | 失效(实参已脱宽) |
| 嵌套行为 | 一刀切全部 readonly | 同样一刀切全部 readonly |
| 对调用方的侵入 | 高 | 低 |
as const 是强制断言,表达式层面一刀切。const T 是泛型层面的推导控制,只影响声明它的那个参数位置。
有个共同点:只要实参已经是变量,两者都会失效。
ts
const arr = [1, 2, 3]
// 类型已经是 number[],脱宽发生在变量声明那一刻
tuple(arr)
// 推导结果就是 number[],const 修饰救不回来
推导链路:
const 类型参数不创造字面量,它只能在字面量出现的那一刻把它接住。
对象和数组行为基本一致:数组变 readonly 元组,对象属性全部 readonly,嵌套结构同样递归处理。
无脑加 const,线上会咬人
网上示例都是"加上 const,类型精确了,真香"。真实项目里踩坑的往往也是这批人。
入参一改就炸
ts
function sortItems<const T extends readonly number[]>(items: T) {
items.push(4)
}
// 报错:Property 'push' does not exist on type 'readonly [...]'
调用方传 [1, 2, 3],T 推成 readonly [1, 2, 3],push 直接不存在。这是线上最容易踩的一个。
处理方式只有三种:去掉 const、内部先 [...items] 复制一份、或者把参数类型写成可变副本。
返回值复用 T,readonly 往外污染
ts
function wrap<const T>(value: T): T {
return value
}
const w = wrap({ a: 1 })
// { readonly a: 1 }
// 调用方拿到的就是 readonly,想覆写直接报错
只要返回值类型里带着 T,readonly 就跟着往外走。工具函数尤其明显:内部锁字面量是好事,对外返回给业务方就变成负担。解法是对外返回展开后的类型,比如 T[number] 或显式构造可变结构,而不是直接返回 T。
联合类型不会因此分发
ts
type Mode = 'dark' | 'light'
function pick<const T extends Mode>(mode: T): T {
return mode
}
const m1 = pick('dark')
// "dark"
const m2 = pick(Math.random() > 0.5 ? 'dark' : 'light')
// "dark" | "light"
const 不会让联合分发。字面量位置保留窄类型,运行时表达式依然推成联合。不要指望它帮你做联合分支收窄,那是控制流分析的事。
readonly 的传染链简化成这样:
TS 5.0 为什么不改默认推导
这才是分水岭。
最直接的原因是兼容性。字面量脱宽是泛型推导的默认契约,直接改等于把所有既有代码的推断结果全部变窄。
假设某天 number[] 自动变成 readonly [1, 2, 3],那 arr.push(4)、arr[0] = 9 这类代码会成片报错。这不是修 bug,是拆生态。
所以 TS 团队选了 opt-in:声明者显式写 const,只影响这一个参数位置。控制权从每个调用点(手写 as const)搬到声明处,写一次,全调用点受益。
代价也不是没有。readonly 元组一多,类型实例化就更重,深层嵌套的大对象尤其明显。这也是为什么不能对着所有泛型参数无脑加 const。
收束
初级开发照着教程复制代码,高级工程师会先判断什么场景该用、什么场景要避开。
能说出"它锁字面量"只是第一层;能说清它和 as const 的边界、readonly 的传染范围、以及 TS 团队为什么把它做成 opt-in,才算真的吃透 TS 5.x 的泛型推导。
写在最后
你被问过最刁钻的 TypeScript 面试题是什么?有没有在业务里踩过 const 类型参数的坑,比如函数内部一改入参就报 readonly?
评论区聊聊。有用的话点个赞。