
写 Go 的人都知道,类型这东西是刻在石头上的。你定义一个 struct,它在内存里长什么样、有几个字段,就死死固定了。如果新接口只需要其中两个字段?很简单,老老实实再建一个 UpdateRequest struct。
但是在 TypeScript 圈子里,画风完全不一样。前端不仅把类型当约束,甚至把它玩成了一门独立的编程语言。这种不写运行时逻辑,纯靠类型推导在编译期疯狂变魔术的操作,被圈内戏称为“类型体操”。
直接看个最常见的场景:提取路由参数,比如从 "/user/:id" 里把 id 拿出来。
如果是 Go,典型的思维是跑到运行时再处理。类型系统在这里基本帮不上忙:
func handleUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"] // 全靠手敲字符串,拼错成 "uid" 编译器根本不管,等跑起来才 panic
}
而在 TS 体操选手的眼里,既然 "/user/:id" 这个字符串在代码里是已知的,为什么不能让编译器在写代码的时候,自动把 id 给解析出来,并且做拼写检查?
于是他们会在类型定义里写出这种东西:
// 定义类型提取规则(注意,这不是可执行代码)
type ExtractParam<Path> = Path extends `${infer _Start}/:${infer Param}` ? Param : never;
// 鼠标悬停在 MyParam 上,编辑器会直接显示 type MyParam = "id"
type MyParam = ExtractParam<"/user/:id">;
// 实际使用时:
let paramName: MyParam;
paramName = "id"; // 正常编译
paramName = "uid"; // 编译直接报错,因为 "uid" 匹配不上 "id"
对于习惯了强类型和极简主义的后端来说,这段代码多少有点让人头皮发麻。但稍微翻译一下,它的逻辑其实很直接:
extends:在 TS 体操里经常被当成if语句用。意思是“判断前面的结构符不符合后面的格式”。${...}:类型级别的字符串模板。infer:可以理解为正则里的“捕获组”。相当于声明一个临时变量Param,把冒号后面的东西塞进去。
连起来就是:如果传进来的路径符合 前面一段/:后面一段 的格式,就把后面那段拿出来当成一个固定的类型,否则报错。
很多后端看到这里会觉得,搞这么复杂,心智负担也太重了。
但 TS 搞出这种图灵完备的类型系统,其实是背上了 JavaScript 的历史包袱。Go 面对的是规规矩矩的静态世界,而 TS 面对的是 JS 这个极其动态、传参毫无章法的烂摊子。
库的作者们在底层做极其复杂的类型体操,其实就是为了给上层业务开发兜底。把脏活累活在编译期干完,换取你在调用函数时,编辑器能极其精准地弹出一个自动补全,告诉你:“这里该传 id 了”。
所以,下次如果看到前端同事对着满屏幕的 <T extends Record<K, V>> 发呆,请理解一下,他们只是在尝试让松散的代码变得更安全一点。
附:如果你想亲自试试水 如果你也被激起了好奇心,想体验一把在编译期“受虐”的快感,强烈推荐去刷一下 GitHub 上著名的开源项目 Type Challenges。里面汇集了从简单到“变态”级别的各种类型体操题目: 🔗 Type Challenges (中文版 README)