Skill Learning
migrate-to-shoehorn
1 · Bilingual Reader 2 · Design Notes
1. Bilingual SKILL.md 左右滑动可直接切换英文版与中文版;两种语言按段落自动对齐。英文中的蓝色虚线词语可点击查看解释。共标记 16 处。
`shoehorn` lets you pass partial data in tests while keeping TypeScript happy . It replaces `as` assertions with type-safe alternatives .
**Test code only .** Never use shoehorn in production code.
Problems with `as` in tests:
- Trained not to use it
- Must manually specify target type
- Double-as (`as unknown as Type`) for intentionally wrong data
```bash
npm i @total-typescript/shoehorn
```
### Large objects with few needed properties
```ts
type Request = {
body: { id: string };
headers: Record<string, string>;
cookies: Record<string, string>;
// ...20 more properties
};
it("gets user by id", () => {
// Only care about body.id but must fake entire Request
getUser({
body: { id: "123" },
headers: {},
cookies: {},
// ...fake all 20 properties
});
});
```
```ts
import { fromPartial } from "@total-typescript/shoehorn";
it("gets user by id", () => {
getUser(
fromPartial({
body: { id: "123" },
}),
);
});
```
### `as Type` → `fromPartial()`
```ts
getUser({ body: { id: "123" } } as Request);
```
```ts
import { fromPartial } from "@total-typescript/shoehorn";
getUser(fromPartial({ body: { id: "123" } }));
```
### `as unknown as Type` → `fromAny()`
```ts
getUser({ body: { id: 123 } } as unknown as Request); // wrong type on purpose
```
```ts
import { fromAny } from "@total-typescript/shoehorn";
getUser(fromAny({ body: { id: 123 } }));
```
| Function | Use case |
| --------------- | -------------------------------------------------- |
| `fromPartial()` | Pass partial data that still type-checks |
| `fromAny()` | Pass intentionally wrong data (keeps autocomplete ) |
| `fromExact()` | Force full object (swap with fromPartial later) |
1. **Gather requirements ** - ask user:
- What test files have `as` assertions causing problems?
- Are they dealing with large objects where only some properties matter?
- Do they need to pass intentionally wrong data for error testing?
2. **Install and migrate**:
- [ ] Install: `npm i @total-typescript/shoehorn`
- [ ] Find test files with `as` assertions: `grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts"`
- [ ] Replace `as Type` with `fromPartial()`
- [ ] Replace `as unknown as Type` with `fromAny()`
- [ ] Add imports from `@total-typescript/shoehorn`
- [ ] Run type check to verify
`shoehorn` 让你能在测试中传入不完整的数据,同时让 TypeScript 保持满意。它用类型安全的替代方案取代 `as` 断言。
**仅限测试代码。** 绝不要在生产代码中使用 shoehorn。
- 训练时被要求不要使用它
- 必须手动指定目标类型
- 对刻意错误的数据使用双重 as(`as unknown as Type`)
```bash
npm i @total-typescript/shoehorn
```
```ts
type Request = {
body: { id: string };
headers: Record<string, string>;
cookies: Record<string, string>;
// ……另外 20 个属性
};
it("按 id 获取用户", () => {
// 只关心 body.id,却必须伪造整个 Request
getUser({
body: { id: "123" },
headers: {},
cookies: {},
// ……伪造全部 20 个属性
});
});
```
```ts
import { fromPartial } from "@total-typescript/shoehorn";
it("按 id 获取用户", () => {
getUser(
fromPartial({
body: { id: "123" },
}),
);
});
```
### `as Type` → `fromPartial()`
```ts
getUser({ body: { id: "123" } } as Request);
```
```ts
import { fromPartial } from "@total-typescript/shoehorn";
getUser(fromPartial({ body: { id: "123" } }));
```
### `as unknown as Type` → `fromAny()`
```ts
getUser({ body: { id: 123 } } as unknown as Request); // 故意使用错误类型
```
```ts
import { fromAny } from "@total-typescript/shoehorn";
getUser(fromAny({ body: { id: 123 } }));
```
| 函数 | 使用场景 |
| --- | --- |
| `fromPartial()` | 传入仍能通过类型检查的不完整数据 |
| `fromAny()` | 传入刻意错误的数据(保留自动补全) |
| `fromExact()` | 强制使用完整对象(之后可与 fromPartial 互换) |
1. **收集需求**——询问用户:
- 哪些测试文件中的 `as` 断言造成了问题?
- 是否在处理只关心部分属性的大对象?
- 是否需要为错误测试传入刻意错误的数据?
2. **安装并迁移**:
- [ ] 安装:`npm i @total-typescript/shoehorn`
- [ ] 查找带 `as` 断言的测试文件:`grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts"`
- [ ] 将 `as Type` 替换为 `fromPartial()`
- [ ] 将 `as unknown as Type` 替换为 `fromAny()`
- [ ] 添加来自 `@total-typescript/shoehorn` 的导入
- [ ] 运行类型检查进行验证
Entry SHA-256: de4da4c11d924fac79c924b2ee3d2db4a5cb98386eb490a599119463d036ae72
2. Why This Skill Is Clever 一句话核心机制
把测试里的无检查 as 断言按意图分流为 fromPartial()、fromAny() 或 fromExact(),用更小的夹具表达测试关注点,同时保留可验证的类型约束与明确的生产代码禁区。
触发与调用边界
直接来源。 frontmatter 将触发条件限定为:用户提到 shoehorn、想替换测试里的 as,或需要 partial test data。它是面向迁移的工作流,不是一般 TypeScript 类型体操教程。
直接来源。 Test code only. Never use shoehorn in production code. 是最强边界。这个 skill 允许测试为了聚焦行为而构造不完整或刻意错误的数据,但不把这种便利扩展到生产输入。
直接来源。 此 skill 自己包含理由、三种迁移模式、决策表、需求收集和验收清单,因此不是纯 wrapper。本报告只展示所选 SKILL.md,没有把软件包实现或其他文件冒充第二份原文。
信息架构 / 执行顺序
先解释迁移动机。 把问题定义为:完整对象造数成本高、目标类型需要手写、负向数据依赖双重断言。
先立禁区。 在安装命令之前声明只允许测试代码使用。
用对照示例建立主路径。 大对象示例展示从大量无关字段缩减到只保留 body.id。
按意图分流。 普通 partial 用 fromPartial();刻意错误的类型用 fromAny();必须完整时用 fromExact()。
执行前收集需求。 先确认文件范围、对象大小和是否存在错误路径测试。
批量迁移后验证。 搜索断言、替换 helper、补导入,最后运行 type check。
最巧妙的设计点
1. 用“测试意图”而不是“语法外形”分类
Pass partial data that still type-checks
直接来源。 决策表不是把所有 as 统一替换成一个函数,而是区分不完整但仍应检查的数据、刻意错误的数据,以及必须完整的数据。
解释。 这避免了典型 codemod 缺陷:字符串替换成功,语义却被抹平。迁移目标从“消灭某个 token”升级为“显式标注每个测试的类型意图”。
2. 先用一个高摩擦示例证明收益
Only care about body.id but must fake entire Request
直接来源。 示例把 20 多个字段的大类型与单一测试关注点并置,迁移后只留下 body.id。
解释。 这是很有效的教学顺序:先让读者看到无关造数噪声,再介绍 helper。工具的价值不是“少写几行”,而是让夹具结构更接近测试断言的真实关注面。
3. 为负向测试保留显式逃生口
wrong type on purpose
直接来源。 fromAny() 专门承接原先的 as unknown as Type,并说明它用于 intentionally wrong data。
解释。 一个只支持“正确 partial”的方案会逼负向测试继续偷偷使用双重断言。给逃生口命名后,审查者能一眼看出哪里在有意越过兼容性检查,也更容易要求相应的错误断言。
4. 极早声明生产边界
Never use shoehorn in production code.
直接来源。 禁令出现在动机之后、安装之前,而不是藏在工作流末尾。
解释。 这阻断了概念外溢:测试 fixture 的便利来自主动放宽“必须完整”的要求;若复制到生产路径,会掩盖真实输入缺失并削弱系统边界。
5. 把迁移收束到机器检查
Run type check to verify
直接来源。 清单没有在添加 import 后结束,而是要求运行类型检查。
解释。 这把“看起来已替换”变成仓库可证明的状态。不过仅 type check 仍不足以证明行为未变,成熟版本还应运行相关测试。
防失败机制与显式不变量
测试专用(直接来源): shoehorn 不能进入 production code。
按意图选择 helper(直接来源): partial、intentionally wrong、full object 分别对应不同函数。
先发现再修改(直接来源): 在安装和迁移前询问受影响文件、对象形态和错误测试需求。
范围可枚举(直接来源): 搜索命令明确限定 *.test.ts 与 *.spec.ts。
类型检查验收(直接来源): 迁移必须以 type check 收尾。
搜索漏报风险(解释): grep -r " as [A-Z]" 捕获不了小写类型别名、泛型、换行断言和多种语法形态;真实执行应使用 AST 工具或更完整的检索复核。
行为回归风险(解释): 类型检查通过不代表测试语义不变,因此还应运行相关测试套件。
Trade-off、可移植性与假设
可读性收益与抽象成本(解释): helper 让意图更显式,但团队必须理解三种函数的差异,并接受额外依赖。
类型安全不是绝对值(解释): fromAny() 仍是受控逃生口;“keeps autocomplete”不等于错误字段会被拒绝。
包与工具链假设(直接来源): 安装命令假设 npm;导入路径绑定 @total-typescript/shoehorn;迁移对象是 TypeScript 测试。
检索环境假设(直接来源): 工作流依赖 grep 及其 --include 行为,Windows 原生环境或不同 shell 需要替代命令。
证据边界(支持性背景): skills/misc/README.md 只确认它是“保留但很少使用、未由插件推广”的 misc skill;本报告未核验 shoehorn 包内部实现,因此不声称 helper 的运行时机制超出原文描述。
可迁移原则
做语法迁移时按语义意图分类,不要把所有表面相似的写法机械替换成同一种抽象。
先用一个能显出噪声与关注点反差的 before/after 示例,再给 API 对照表。
任何放宽约束的测试 helper 都要紧邻一条生产代码禁区。
为必要的逃生口取一个审查时可见的名字,不要让它继续藏在双重断言里。
工作流必须以机器可检查的验收步骤结束,并补足该检查覆盖不到的行为验证。
今日实践题
选一个使用 as unknown as Type 的测试:它真的是“刻意错误的数据”吗?请写出该测试期待触发的错误路径;如果写不出来,应该改用 fromPartial()、fromExact(),还是删除这次强制转换?
Evidence note
直接来源: 触发条件、测试专用边界、三个 helper 的适用场景、before/after 示例、需求问题和迁移清单均来自本次选择的 skills/misc/migrate-to-shoehorn/SKILL.md。
支持性背景: skills/misc/README.md 仅用于确认该条目属于未推广且很少使用的 misc inventory,不属于所选原文。
解释: “语义迁移”“受控逃生口”、grep 漏报与行为回归风险是根据来源结构作出的工程判断。
Source & provenance
Repository path skills/misc/migrate-to-shoehorn/SKILL.md
Generated 2026-08-28 12:38 Asia/Shanghai
Upstream commit 6654f6b60cd9d5be8b54c6fafe44346dabeb3b76