- 教育
- 前端
- 后端
【免费下载链接】typehero
Connect, collaborate, and grow with a community of TypeScript developers
本篇技术指南围绕 TypeHero 仓库中 Advent of TypeScript 2024 第 22 天挑战题("Reindeer Sudoku")展开:题目要求读者仅凭类型层面编写一个名为Validate的谓词类型(predicate),判断一个用 9 种驯鹿 emoji 填满的 9×9 数独棋盘是否符合行、列、宫三大约束。读完本文,你将掌握如何在 TypeScript 类型系统中用元组(tuple)、递归条件类型与字符串字面量类型实现复杂的矩阵校验,并理解 TypeHero 挑战题的"题目—用户骨架—测试断言—校验脚本"完整链路。
挑战背景:驯鹿的恶作剧
圣诞老人的九只驯鹿——Dasher(💨)、Dancer(💃)、Prancer(🦌)、Vixen(🌟)、Comet(☄️)、Cupid(❤️)、Donner(🌩️)、Blitzen(⚡)和 Rudolph(🔴)——这次决定把自己排列成一块数独棋盘来捣乱。它们在开摆之前还给圣诞老人留下一段预言式的消息:
SaNtA.... yOu MuSt ImPleMeNt ThE
ValidateTyPe To DeTerMinE WhEThEr OuR SuDokU ConFiGuRaTiOn Is vALid
更"贴心"的是,Vixen 还单独留了一张字条:
make sure
Validateis a predicate
- Vixen
这里的 "predicate"(谓词)是计算机科学中对"返回true或false的函数"的称呼。放到类型层面,这意味着Validate必须是一个条件类型(conditional type),对合法棋盘解析为字面量类型true,对非法棋盘解析为false,而不是boolean这种宽泛联合。这一点在 tests.ts 中通过Equal<test_sudoku_x_actual, true | false>形式的严格断言得到了强制。
Sudoku 规则回顾
题目在 prompt.md 中完整复述了数独的经典规则:
- 网格结构:游戏在 9×9 的网格上进行,网格被划分为九个 3×3 的子网格,即"宫"(region)。
- 数字填充:目标是用 1 到 9 的数字填满整个网格。
- 行约束:每一行必须包含 1 到 9 的每个数字,不得重复。
- 列约束:每一列也必须包含 1 到 9 的每个数字,不得重复。
- 宫约束:九个 3×3 的宫各自必须包含 1 到 9 的每个数字,同样不得重复。
通常玩家需要通过逻辑推演填出空格,确保行、列、宫都满足规则;而本题反其道而行——所有格子已经全部填满,我们的任务只是判断给定配置是否符合数独规则,即从"求解"变成"验证"。
题目给出的类型骨架
挑战目录 challenges/aot/2024/22 下包含五个关键文件,其中 user.ts 预先声明了九只驯鹿的类型别名及其语义注释:
/** because "dashing" implies speed */ type Dasher = '💨'; /** representing dancing or grace */ type Dancer = '💃'; /** a deer, prancing */ type Prancer = '🦌'; /** a star for the dazzling, slightly mischievous Vixen */ type Vixen = '🌟'; /** for the celestial body that shares its name */ type Comet = '☄️'; /** symbolizing love, as Cupid is the god of love */ type Cupid = '❤️'; /** representing thunder, as "Donner" means thunder in German */ type Donner = '🌩️'; /** meaning lightning in German, hence the lightning bolt */ type Blitzen = '⚡'; /** for his famous red nose */ type Rudolph = '🔴'; type Reindeer = | Dasher | Dancer | Prancer | Vixen | Comet | Cupid | Donner | Blitzen | Rudolph;每个 emoji 与驯鹿的名字一一对应(例如 Donner 在德语中意为"雷",故用 🌩️;Blitzen 在德语中意为"闪电",故用 ⚡),因此Reindeer联合类型恰好有 9 个成员——这为后续"每种符号恰好出现一次"的去重校验提供了天然基础。
待实现的核心类型是一个占位符:
type Validate = unknown;你需要把它替换成自己的实现。注意题目的语义是"验证棋盘是否合法",即Validate接收一个 9×9 的 emoji 矩阵,并作为谓词返回true或false。
测试断言:六个棋盘,三对三错
tests.ts 定义了 6 个测试用例,前 3 个断言Validate<...>等于true,后 3 个断言等于false。它们通过type-testing包中的Equal与Expect工具进行严格相等检查:
import { Equal, Expect } from 'type-testing'; type test_sudoku_1_actual = Validate<[ [['💨', '💃', '🦌'], ['☄️', '❤️', '🌩️'], ['🌟', '⚡', '🔴']], [['🌟', '⚡', '🔴'], ['💨', '💃', '🦌'], ['☄️', '❤️', '🌩️']], // ...共 9 行 ]>; type test_sudoku_1 = Expect<Equal<test_sudoku_1_actual, true>>;逐用例分析可以提炼出验证器必须能识别的非法模式:
- test_sudoku_1/2/3(期望
true):三个棋盘均为合法解。以 test_sudoku_1 为例,它是标准的"三行一循环"拉丁方结构(每 3 行一个宫行块,符号在块内轮转),行、列、宫全部无重复。 - test_sudoku_4(期望
false):与 test_sudoku_1 几乎完全相同,但第 7 行第 3 列的符号从💨被篡改为🌟(见 tests.ts 第 53 行)。这直接导致第 7 行内🌟重复、第 3 列内🌟重复,且右下角宫(第 7~9 行、第 7~9 列)出现两个🌟——一个改动同时触发三条约束违规。 - test_sudoku_5(期望
false):与 test_sudoku_2 几乎相同,但第 7 行的第三个元素从💃被改成🦌(第 67 行),使该行出现两个🦌。 - test_sudoku_6(期望
false):一个明显更"混乱"的棋盘,例如第 1 行就出现💨两次、🌟一次;且第 4 行第 1 列与第 5 行第 5 列等位置存在跨行重复,说明验证器必须同时检查行、列、宫三个维度,任何单维度校验都放不过这些用例。
从测试结构看,合法的正样本大多采用"同一宫行块内三个宫为同一符号三元组的轮换排列",这暗示一个高效的实现思路:每个宫恰好是某组三个符号的全排列,且每个宫行块与宫列块内部保持轮换。但更通用、更稳妥的做法是显式校验三大约束(见下文)。
核心解题思路:类型级三大约束校验
Validate的输入是T extends Reindeer[][](9 行 × 9 列,每行 9 个元素,总长 81)。在类型系统里实现数独验证,可以拆成三个相互独立的工具类型,然后取"与":
1. 行校验:检查每行无重复
把一行(长度为 9 的元组)的所有元素做去重,再比较去重前后长度是否一致:
type ToUnion<T extends unknown[]> = T[number]; type HasDuplicates<T extends unknown[]> = Equal<ToUnion<T>, T[0]> extends true ? false // 单元素或空行不可能重复 : /* 用递归逐行拆分判断 */ ...; type AllRowsUnique<T extends Reindeer[][]> = T extends [infer Head extends Reindeer[], ...infer Tail extends Reindeer[][]] ? RowIsUnique<Head> extends true ? AllRowsUnique<Tail> : false : true;这里的核心技巧是借助"元组联合去重后长度是否变短"或"逐一弹出元素并检查剩余部分是否仍包含该元素"来判断重复。例如一个通用的无重复判定可以写成:
type HasDup<T extends unknown[], Acc extends unknown[] = []> = T extends [infer Head, ...infer Tail] ? Head extends Acc[number] ? true : HasDup<Tail, [...Acc, Head]> : false;2. 列校验:先转置,再复用行校验
类型系统没有直接的"按列取值"语法,但可以定义一个Transpose工具类型,把 9×9 矩阵转置成 9×9(第 i 行变为原第 i 列),随后对转置结果复用AllRowsUnique:
type Transpose<M extends Reindeer[][]> = { [I in keyof M]: { [J in keyof M]: M[J][I] } };其中keyof M产生的0 | 1 | ... | 8索引键会在映射类型中被逐一实例化,从而完成行列互换。
3. 宫校验:重塑为宫,再复用无重复校验
3×3 宫的提取可以这样实现:先把棋盘按 3 行一组分组(Chunk<M, 3>),每一组是 3 行;再把组内 3 行按每 3 列切片并拼接,得到该组对应的 3 个宫;最终得到一个 9 宫数组,每个宫是 9 个符号的一维元组。之后同样用HasDup逐个检查。
这三个子校验各自产出布尔值,最后用条件类型做"与":
type Validate<T extends Reindeer[][]> = AllRowsUnique<T> extends true ? AllColumnsUnique<T> extends true ? AllRegionsUnique<T> extends true ? true : false : false : false;从题目的 Vixen 提示看,最终解析结果必须是字面量true/false,而不能是boolean——这也是谓词(predicate)在类型层面的准确翻译:Validate不是返回"某个值",而是让类型系统在两条不同分支上分别解析出唯一的真/假字面量。
挑战的运行机制与工程细节
理解这道题如何被验证,能帮你判断自己实现的正确性。仓库根目录的 challenges/validate.ts 是挑战的校验脚本,它做了两件事:
- 元数据校验:遍历
challenges/与challenges/aot/下所有挑战目录,用 Ajv 依据metadata.schema.json校验metadata.json,并检查目录名与id一致、prerequisites是合法 id 等。本题的 metadata.json 声明id为"2024-22"、难度为"event"、作者为"TypeHero"、无前置依赖。 - 编译级测试:读取每个挑战的
tsconfig.json编译器选项,将solutions/下的答案源码与tests.ts拼接成一个内存中的虚拟源文件(createSourceFile生成,配合自定义CompilerHost的getSourceFile拦截),再用 TypeScript 编译器 APIcreateProgram+getPreEmitDiagnostics收集诊断,无错误即输出绿色的✓ challenges/<id>/solutions/<file>。
因此,一个通过验证的实现必须满足两点:测试用例中的Expect<Equal<...>>全部成立(即解析出的字面量类型精确匹配),且整个拼接文件在编译期零诊断。
本题的 tsconfig.json 开启了三项严格选项:
{ "compilerOptions": { "strict": true, "exactOptionalPropertyTypes": true, "noUncheckedIndexedAccess": true } }其中strict开启strictNullChecks等系列检查,要求类型实现不能依赖隐式any;noUncheckedIndexedAccess意味着T[number]、M[J][I]这类索引访问在元素可能为undefined时不会静默通过,这恰恰提醒我们在类型实现中要处理元组越界/缺省的情况。目前 solutions/1.ts 在本仓库中为空文件,留待读者自行作答——你可以在本地运行仓库的校验脚本(如pnpm validate或直接执行challenges/validate.ts)来实时验证你的Validate实现是否通过全部 6 个断言。
总结
"Reindeer Sudoku" 是 Advent of TypeScript 系列中一道极具代表性的类型级算法题:它把数独的三大约束(行、列、宫)完整翻译到 TypeScript 类型空间,要求用元组拆分、递归条件类型、索引访问与字面量联合去重来实现一个严格的谓词类型。通过本题,你可以系统掌握:
- 谓词类型(predicate type)在类型层面的含义:解析为精确的
true/false字面量; - 用递归条件类型遍历任意深度的嵌套元组结构;
- 用"去重后长度是否变化"或"逐元素累积比对"判断元组无重复;
- 用映射类型实现矩阵转置与分组(chunk)等结构性变换;
- 理解 TypeHero 挑战仓库"题目(prompt)— 骨架(user)— 断言(tests)— 编译校验(validate)"四位一体的工程闭环,以及 challenges/validate.ts 中基于 TypeScript Compiler API 的内存编译验证机制。
如果你正在刷 Advent of TypeScript 或练习类型体操,把这道题的三个子校验分别实现、再组合成一个Validate,会比直接照抄任何答案更能建立对类型系统递归与映射能力的直觉。
- 教育
- 前端
- 后端
【免费下载链接】typehero
Connect, collaborate, and grow with a community of TypeScript developers
相关推荐
用 TypeScript 类型系统实现 Connect 4:TypeHero Advent of TypeScript 2024 第 23 天挑战全解
用 TypeScript 类型系统实现 Connect 4:TypeHero Advent of TypeScript 2024 第 23 天挑战全解 本篇技术
教育前端后端TypeHero Advent of TypeScript 2024 第 24 天:用 TypeScript 类型系统实现迷宫求解器 Move
TypeHero Advent of TypeScript 2024 第 24 天:用 TypeScript 类型系统实现迷宫求解器 Move 导读 本文是 T
教育前端后端用 TypeScript 类型系统实现井字棋:TypeHero Advent of TypeScript 2023 第 21 天挑战深度解析
用 TypeScript 类型系统实现井字棋:TypeHero Advent of TypeScript 2023 第 21 天挑战深度解析 本文以 TypeH
教育前端后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考