TS const 类型参数一道面试题的四层追问

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 修饰救不回来

推导链路:

flowchart TD A[调用泛型函数] --> B{参数带 const} B -->|否| C[默认推断] B -->|是| D{实参是字面量} D -->|是| E[保留字面量类型] D -->|否| F[沿用实参已有类型] E --> G[数组变 readonly tuple] E --> H[对象属性变 readonly] C --> I[宽类型 number string]

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 的传染链简化成这样:

flowchart LR A[const T] --> B[顶层字面量收窄] B --> C[数组元素逐个收窄] C --> D[嵌套对象全部 readonly] D --> E[返回值带回 readonly] E --> F[调用方 push 报错]

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?

评论区聊聊。有用的话点个赞。

相关推荐
晚安日记wanna1 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
掰头战士1 小时前
你说你折这玩意干什么-agent工作的核心,还真得好好考虑怎么折叠上下文
typescript·llm·agent
用户921080262861 小时前
前端 Vue 专栏 09:Vue Router 与前端路由
前端
晚安日记wanna1 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构
SamDeepThinking1 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
ynchyong1 小时前
VUE 中 不能将类型“R[]”分配给类型“UnwrapRefSimple<R>[]”
typescript·vue·ts·unwraprefsimple
CC数分1 小时前
2026年金融数据分析岗位需要哪些工具?2027届秋招备考指南
面试·数据分析
無限進步D2 小时前
Java Web 前端 简介
java·开发语言·前端·css·html·css3·web
码士集团小青2 小时前
从落地到“半停用“:qiankun 微前端在一个 Vue 3 + Vite 项目中的完整实践与反思
前端