Skip to content
团子云技术 Lite 1.048596
Go back

写惯了 Go 的人,可能很难理解 TypeScript 的“类型体操”有多离谱

写 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"

对于习惯了强类型和极简主义的后端来说,这段代码多少有点让人头皮发麻。但稍微翻译一下,它的逻辑其实很直接:

连起来就是:如果传进来的路径符合 前面一段/:后面一段 的格式,就把后面那段拿出来当成一个固定的类型,否则报错。

很多后端看到这里会觉得,搞这么复杂,心智负担也太重了。

但 TS 搞出这种图灵完备的类型系统,其实是背上了 JavaScript 的历史包袱。Go 面对的是规规矩矩的静态世界,而 TS 面对的是 JS 这个极其动态、传参毫无章法的烂摊子。

库的作者们在底层做极其复杂的类型体操,其实就是为了给上层业务开发兜底。把脏活累活在编译期干完,换取你在调用函数时,编辑器能极其精准地弹出一个自动补全,告诉你:“这里该传 id 了”。

所以,下次如果看到前端同事对着满屏幕的 <T extends Record<K, V>> 发呆,请理解一下,他们只是在尝试让松散的代码变得更安全一点。


附:如果你想亲自试试水 如果你也被激起了好奇心,想体验一把在编译期“受虐”的快感,强烈推荐去刷一下 GitHub 上著名的开源项目 Type Challenges。里面汇集了从简单到“变态”级别的各种类型体操题目: 🔗 Type Challenges (中文版 README)


Share this post on:

Previous Post
我们的技术经验正在被“做空”:写在 OpenClaw 刷屏后的 FOMO 时刻
Next Post
什么是 KTLO (Keep The Lights On)?