第一次看到有人用 TypeScript 的类型系统解数独,我的第一反应是:这人是认真在写代码,还是单纯想折磨 TypeScript 编译器?
那堆代码里没有任何 runtime 逻辑,全是 type、infer、extends、递归条件类型。我在编辑器里 hover 上去,发现编译期居然真的把残缺的九宫格一步步填满了。为了彻底搞懂它,我把整段代码丢给 DeepSeek,让它逐行讲、反复问,最后整理出了下面这套从零开始的解读。它不只是一次炫技,更是一次对 TypeScript 类型机制的极限训练。这篇内容适合写过类型体操、维护过复杂类型定义,或者单纯想看看"类型系统到底能跑多快"的人。
1. "类型即程序":这算哪门子数独求解?
先别急着看代码,把概念理顺:当我们说"用 TypeScript 的类型求解数独",到底是在哪一层"运行"?
普通程序跑在 JavaScript 引擎里,值是字符串、数字、对象。而类型程序跑在 tsc 里,输入是类型,输出还是类型。你可以把编译器当作一台微型虚拟机,type 就是它的指令集,条件类型就是它的 if,递归类型就是循环,infer 就是模式匹配。数独求解本质上是搜索和回溯,这种算法天然可以用递归来表达,而 TypeScript 的条件类型又支持递归,所以理论上任何可计算问题都能塞进类型系统。关键不是"能不能",而是"怎么写才不会被编译器卡死"。
这段代码之所以让人看得头皮发麻,是因为它把类型系统的一切表达力都压在了一张九宫格上:
- 模板字面量类型用来编码棋盘坐标。
- 递归条件类型用来遍历空格、尝试数字。
- 分布式条件类型用来模拟"对一个数字集合做分支尝试"。
never用来表示"此路不通"。infer用来从大类型里拆出子类型。
很多人把这类代码当成"类型体操表演赛",但它其实有实际价值。比如写表单 schema 校验时,你可以把规则直接建模到类型层面,让非法数据在编译期就暴露;写数据库驱动时,SQL 语句的拼接结果可以完全由类型推导出来。理解了类型级数独,回头看这些场景会轻松很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把普通解法翻译成类型:从数组回溯到递归条件类型
2.1 普通回溯算法长什么样
一道标准数独的解法,通常是这么写的:
- 找到第一个空白格。
- 依次尝试填入
1~9。 - 每填一个数字,检查行、列、宫有没有冲突。
- 没有冲突就递归进入下一个空格。
- 如果 9 个数字都试完仍然走不通,就回溯到上一格,换一个数字继续。
这是一个教科书式的深度优先搜索。如果你把"棋盘数组"换成"棋盘类型",把"数组索引"换成"模板字面量拼接出来的坐标键",把"循环"换成"递归条件类型",整个算法就能原封不动地搬进类型系统。
2.2 值空间和类型空间的映射关系
我先用一张表格把对应的关系列出来,阅读理解整个代码时会轻松很多:
| 普通解法 | 类型空间里的对应物 |
|---|---|
棋盘数组 string[][] |
Board extends Record<CellKey, Digit | '.'> |
空白格坐标 [row, col] |
字符串键 '00' '45' 等 |
| 遍历找空格 | 用 infer 拆解棋盘对象的键 |
| 检查行/列/宫冲突 | UsedInRow、UsedInCol、UsedInBox 三个条件类型 |
| 尝试 1~9 | 分布式条件类型 C extends Digit,自动展开联合类型 |
| 回溯失败 | 递归分支最终返回 never |
表格看着简单,但实际代码里的每一步都要比普通数组操作繁琐十倍,因为类型系统没有"循环变量",一切都要通过类型间的运算来表达。
2.3 一个最小例子:检查某行是否有重复数字
比如要检查某一行里是否已经存在数字 N,普通代码是 row.includes(N)。类型代码就得先把这一行所有格子里的数字取出,合并成一个联合类型,再判断 N 是否属于这个联合类型。
typescript复制type RowKey = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8';
type CellKey = `${RowKey}${RowKey}`;
type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Board = { [K in CellKey]: Digit | '.' };
// 生成同一行所有格子键的辅助类型
type KeysInRow<R extends RowKey> = `${R}${RowKey}`;
// 提取一行里的所有数字,把 '.' 过滤掉
type UsedInRow<R extends RowKey, B extends Board> =
KeysInRow<R> extends infer K
? K extends CellKey
? B[K] extends Digit
? B[K]
: never
: never
: never;
// 判断数字 N 是否已在某行中出现
