Skill Learning

migrate-to-shoehorn

1. Bilingual SKILL.md

左右滑动可直接切换英文版与中文版;两种语言按段落自动对齐。英文中的蓝色虚线词语可点击查看解释。共标记 16 处。

入口 skill:migrate-to-shoehorn左右滑动切换语言 · 段落位置自动对齐
# Migrate to Shoehorn
## Why shoehorn?
`shoehorn` lets you in tests while . It replaces `as` assertions with .
**.** 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
## Install
```bash npm i @total-typescript/shoehorn ```
## Migration patterns
###
Before:
```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 getUser({ body: { id: "123" }, headers: {}, cookies: {}, // ...fake all 20 properties }); }); ```
After:
```ts import { fromPartial } from "@total-typescript/shoehorn"; it("gets user by id", () => { getUser( fromPartial({ body: { id: "123" }, }), ); }); ```
### `as Type` → `fromPartial()`
Before:
```ts getUser({ body: { id: "123" } } as Request); ```
After:
```ts import { fromPartial } from "@total-typescript/shoehorn"; getUser(fromPartial({ body: { id: "123" } })); ```
### `as unknown as Type` → `fromAny()`
Before:
```ts getUser({ body: { id: 123 } } as unknown as Request); // ```
After:
```ts import { fromAny } from "@total-typescript/shoehorn"; getUser(fromAny({ body: { id: 123 } })); ```
## When to use each
| Function | Use case | | --------------- | -------------------------------------------------- | | `fromPartial()` | that | | `fromAny()` | Pass () | | `fromExact()` | (swap with fromPartial later) |
## Workflow
1. **** - 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 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` - [ ]
# 迁移到 Shoehorn
## 为什么使用 shoehorn?
`shoehorn` 让你能在测试中传入不完整的数据,同时让 TypeScript 保持满意。它用类型安全的替代方案取代 `as` 断言。
**仅限测试代码。** 绝不要在生产代码中使用 shoehorn。
测试中使用 `as` 的问题:
- 训练时被要求不要使用它 - 必须手动指定目标类型 - 对刻意错误的数据使用双重 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,没有把软件包实现或其他文件冒充第二份原文。

信息架构 / 执行顺序

  1. 先解释迁移动机。 把问题定义为:完整对象造数成本高、目标类型需要手写、负向数据依赖双重断言。
  2. 先立禁区。 在安装命令之前声明只允许测试代码使用。
  3. 用对照示例建立主路径。 大对象示例展示从大量无关字段缩减到只保留 body.id。
  4. 按意图分流。 普通 partial 用 fromPartial();刻意错误的类型用 fromAny();必须完整时用 fromExact()。
  5. 执行前收集需求。 先确认文件范围、对象大小和是否存在错误路径测试。
  6. 批量迁移后验证。 搜索断言、替换 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 仍不足以证明行为未变,成熟版本还应运行相关测试。

防失败机制与显式不变量

Trade-off、可移植性与假设

可迁移原则

  1. 做语法迁移时按语义意图分类,不要把所有表面相似的写法机械替换成同一种抽象。
  2. 先用一个能显出噪声与关注点反差的 before/after 示例,再给 API 对照表。
  3. 任何放宽约束的测试 helper 都要紧邻一条生产代码禁区。
  4. 为必要的逃生口取一个审查时可见的名字,不要让它继续藏在双重断言里。
  5. 工作流必须以机器可检查的验收步骤结束,并补足该检查覆盖不到的行为验证。

今日实践题

选一个使用 as unknown as Type 的测试:它真的是“刻意错误的数据”吗?请写出该测试期待触发的错误路径;如果写不出来,应该改用 fromPartial()、fromExact(),还是删除这次强制转换?

Evidence note

Source & provenance
Repository path
skills/misc/migrate-to-shoehorn/SKILL.md
Generated
2026-08-28 12:38 Asia/Shanghai
Upstream commit
6654f6b60cd9d5be8b54c6fafe44346dabeb3b76
Source
View on GitHub