any 是一个会传染的洞
`any` 的语义不是「我不知道」,而是「关掉类型检查」。一旦某个函数返回 `any`,整条调用链上的推导都会退化:下游变量变成 `any`,传参不再报错,重命名不再跳转,重构变成纯手工。真正的代价不是编译慢,而是你失去了唯一那份能自动发现空指针和字段拼写错误的机制。
如果只是为了绕过第三方库缺类型声明,正确的做法是造一个窄接口把 `any` 关在边界上,而不是让它进入业务层。一旦进入,删除它需要的成本通常是指数级的——因为没人能列全所有受影响的调用点。
真实项目里常见的顺序是:先有一个 `any`,接着出现 `any[]` 和 `Promise<any>`,最后整条数据流失去保护。判断标准很简单:如果把 `any` 替换成 `unknown` 会立刻爆出一百个错误,说明这条链路早就没有类型保障了。
// 反例:把 unknown 丢进业务层
function parse(input: string): any {
return JSON.parse(input)
}
// 正例:边界处收窄,内部保持类型安全
type Config = { port: number; host: string }
function parseConfig(input: string): Config {
const raw: unknown = JSON.parse(input)
if (typeof raw !== "object" || raw === null) throw new TypeError("config must be an object")
const port = (raw as Record<string, unknown>).port
const host = (raw as Record<string, unknown>).host
if (typeof port !== "number" || typeof host !== "string") {
throw new TypeError("config.port must be number and config.host must be string")
}
return { port, host }
}判别联合:让状态机成为类型
当一个对象有多种形态(加载中、成功、失败)时,用可选字段表示会带来无穷组合:`loading` 和 `error` 同时为真该怎么办?判别联合给每个分支一个字面量 tag,编译器就能检查分支是否穷尽、字段是否在错误的分支里被访问。
判别联合最大的收益是 `switch` 的穷尽性检查。只要你把联合类型的所有 tag 都列出来却漏了一个分支,`never` 断言会立刻报错。这意味着新增一种状态时,编译器会列出所有需要跟着改的 switch,而不是让你在运行期才发现漏处理。
注意 `never` 断言的位置要放在 switch 的 default 分支,并且用 `const _exhaustive: never = state` 这样的形式,而不是在每个 case 里写。否则一旦某个 case 的类型被收窄为空对象,你就失去了检查。
type RequestState =
| { status: "idle" }
| { status: "loading"; startedAt: number }
| { status: "success"; data: string }
| { status: "error"; message: string }
function render(state: RequestState): string {
switch (state.status) {
case "idle":
return "尚未开始"
case "loading":
return `已耗时 ${Date.now() - state.startedAt}ms`
case "success":
return state.data
case "error":
return `失败:${state.message}`
default: {
const _exhaustive: never = state
return _exhaustive
}
}
}
// 新增 { status: "cancelled" } 后,所有遗漏分支的 switch 都会编译报错泛型约束:让 API 说明它真正的要求
不带约束的 `<T>` 等于 `<T>` 加一个隐藏的 any:函数里对 T 调用任何属性都不报错,调用方却可能传入原始类型。写 `T extends Record<string, unknown>` 只是起点,真正的收益来自把约束和返回值绑定,让 T 的能力穿过返回值继续传播。
实践中常用的三种模式是:约束 + 默认值(给调用方一个可用的起点)、约束 + 索引签名(允许任意键但值类型受控)、以及条件类型做输入到输出的映射(如 `ReturnType`、框架的 `MaybePromise<T>`)。第三种最容易被滥用,因为它容易把编译时间拖垮。
约束的价值在于「早失败」。一个约束良好的泛型函数在编译期就能拒绝错误的调用方,让错误停在边界;一个没有约束的泛型函数则把错误推迟到运行期,甚至推迟到生产环境才暴露。
type Mapper<T extends Record<string, unknown>> = {
keys: () => Array<keyof T>
get<K extends keyof T>(key: K): T[K] | undefined
set<K extends keyof T>(key: K, value: T[K]): Mapper<T>
}
function createMapper<T extends Record<string, unknown> = { id: string }>(initial: T): Mapper<T> {
const store = new Map<keyof T, T[keyof T]>(Object.entries(initial) as Array<[keyof T, T[keyof T]]>)
return {
keys: () => Array.from(store.keys()),
get: (key) => store.get(key),
set: (key, value) => {
store.set(key, value)
return createMapper(Object.fromEntries(store) as T)
},
}
}
// 传入 number 会立刻报错:Mapper<number> 不满足 Record<string, unknown>收窄为什么失败:五类常见断点
第一种是对可选属性用 `if (obj.key)` 却忘了 `undefined` 分支;第二种是对数组用 `typeof` 或直接判空,而 TypeScript 只对少数类型做自动收窄;第三种最隐蔽:赋值给已声明为联合类型的变量时,TS 不会跨语句保留分支信息,需要显式断言或重新判断。
第四种是 `const` 与 `let` 的差异。如果你用 `let` 持有一个联合类型,TypeScript 会在函数调用之间假设它可能被改写,因此拒绝收窄;改用 `const` 或在分支内先赋给新 `const`,收窄就能穿透。
第五种是导入 `type` 与 `interface` 混用导致的结构不一致,以及 `@ts-expect-error` 把真实错误压掉。排查顺序建议固定:先确认变量是不是 `const`,再确认判别字段是不是字面量类型(别用 `string`),最后才考虑 `as` 断言。
type Box = { kind: "a"; a: number } | { kind: "b"; b: string }
// 失败写法:kind 声明成 string,无法收窄
function bad(box: { kind: string }) {
if (box.kind === "a") {
// 这里 box.a 不存在,因为 kind 不是字面量类型
}
}
// 正确写法:用 as 收窄到联合类型
function good(box: Box) {
if (box.kind === "a") {
return box.a + 1
}
return box.b.length
}
// 排查三连:const 了吗?kind 是字面量吗?分支赋值过吗?把类型约束接进工程流程
类型设计要落地,离不开三处硬性设置:`strict: true` 全量开启、`noUncheckedIndexedAccess: true` 防止数组与索引访问返回 `undefined`、以及把 `eslint` 的 `@typescript-eslint/no-explicit-any` 与 `ban-ts-comment` 设为 error 让 `any` 和 `@ts-ignore` 无法悄悄混入。
第二个关键是控制公共 API 的导出面。内部函数可以放心用断言,但凡是要跨包复用的类型,都应该是显式声明的接口;不要把后端响应类型直接 `typeof response` 导出,因为内部字段一旦变更,所有下游编译期无感知地坏掉。
最后用 `satisfies` 做双向校验:它要求对象满足目标类型,同时保留推断出的字面量类型。写路由表、事件映射表这类「既要类型正确又不想丢具体键」的场景,`satisfies` 比 `as const` 更安全,因为它不会替你撒谎。
// tsconfig.json 关键项
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true
}
}
// 路由表:satisfies 同时保证键完整与字面量推断
const routes = {
home: { title: "首页", auth: false },
admin: { title: "管理", auth: true },
} satisfies Record<string, { title: string; auth: boolean }>
type RouteKey = keyof typeof routes // "home" | "admin",不丢失具体键