name: migrate-to-shoehorn description: "把测试中的 as 类型断言迁移到 @total-typescript/shoehorn。用户提到 shoehorn、希望替换测试中的 as,或需要部分测试数据时使用。"
在 TypeScript 测试里表达局部或错误输入
这是作者特定技术栈的专项迁移工具,用于减少测试中随意的类型断言。
还不清楚 Skill、Agent、安装和调用?先读 从零开始的6节入门课。
在关系图中查看 migrate-to-shoehorn 与其他技能的关联 →
先把必要的概念讲清楚
这课涉及 TypeScript 测试数据。核心困难是测试往往只关心大对象的少数字段,却被类型要求迫使构造大量无关数据。
下面是老师补充的入门说明;原作者的要求保留在中英对照正文中。所有例子均为帮助理解而构造的教学情境。
package / dependency|代码包与依赖
package 是可以统一安装、导入或管理的一组代码;dependency 是当前程序需要的其他代码。开发依赖常用于检查和构建。安装了一个包只说明代码可取得,还要配置并调用,才会实际参与工作。
fixture / harness|测试样本与运行装置
fixture 是为复现或测试准备的已知输入和初始数据,例如固定课程清单。harness 是把代码、输入和检查串起来的运行装置,例如一次命令启动最小服务并断言结果。它们帮助每次在相同条件下比较,而不是凭上次页面看起来怎样判断。
mock|替代真实依赖的测试对象
测试时用可控制的对象替代真实服务,例如让模型调用固定返回一句答案。这样可以稳定检查程序如何处理响应,但无法据此证明真实模型总能生成好答案。过度替代内部组件还会让测试和内部写法绑定,一重构就要改测试。
interface / API|接口
使用者与一项能力交互时遵循的约定,包括可调用什么、传入什么、得到什么、失败怎样表示。接口可以是程序函数,也可以是网络请求。创建收藏接口接收用户和课程信息、返回收藏结果;它不需要向调用者暴露数据库表结构。这里的接口通常不是指页面外观。 本教材涉及更广义的“接口”时,还包括错误、调用顺序和约束等使用约定;不能把接口一概等同于网络 API。
读原文,理解每一步为什么这样做
左右内容按小节对应;窄屏先中文、后英文。两种语言均完整展示,对应讲解紧接在小节之后。译文传达原文要求;老师讲解补充概念、原因、例子与适用边界。
name: migrate-to-shoehorn description: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.
用 Shoehorn 改善测试数据的类型写法
Migrate to Shoehorn
为什么使用 shoehorn
shoehorn 让测试可以传入部分数据,同时让 TypeScript 接受这种构造。作者用它替代一些直接写 as 的类型断言。
只用于测试代码,不要用于正式产品代码。
直接使用 as 的不便包括:开发者通常被建议少用断言;必须手工指定目标类型;为了故意传错误数据,还常要写 as unknown as Type。
Why shoehorn?
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
类型断言不会替你检查运行时数据
TypeScript 的 as 是告诉类型系统把某个值按指定类型看待,不是把对象在运行时补完整,也不是验证它符合外部真实数据。
例如 Request 类型有二十个字段,但测试只关心 body.id。手工伪造其余字段很繁琐;直接断言成 Request 虽方便,却可能让不正确的数据看起来合法。
shoehorn 提供明确表达“这是部分测试数据”的函数。它的价值在测试构造便利与类型提示,不是让缺失字段在真实运行时自动存在。
安装
npm i @total-typescript/shoehorn
Install
npm i @total-typescript/shoehorn
常见迁移方式
Migration patterns
对象很大,但只关心其中几个字段
迁移前,测试只关心 body.id,却要伪造请求对象中其他很多字段:
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
});
});
迁移后,通过 fromPartial 只提供相关部分:
import { fromPartial } from "@total-typescript/shoehorn";
it("gets user by id", () => {
getUser(
fromPartial({
body: { id: "123" },
}),
);
});
Large objects with few needed properties
Before:
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
});
});
After:
import { fromPartial } from "@total-typescript/shoehorn";
it("gets user by id", () => {
getUser(
fromPartial({
body: { id: "123" },
}),
);
});
fromPartial、fromAny、fromExact 各表达什么意图
fromPartial 用于只提供相关字段,同时对提供的内容保留类型约束;fromAny 用于故意传入错误数据,以检验错误处理;fromExact 用于需要完整对象的情况。
**例子:**正常查询用户时 body.id 应是字符串,可用 fromPartial 提供 id。想验证“数字 ID 应被拒绝”,则故意输入数字;这不是让生产逻辑接受错误类型,而是测试它如何失败。
阅读代码时看函数名就应知道样本意图。fromAny 不是偷懒消除所有类型报错的通行证,尤其不应进入正式业务代码。
不完整的测试对象,不会在运行时自动变成完整对象
测试一个按用户编号查找的函数,可能只需要 body.id。为了满足完整请求类型,填写几十个无关字段,会让测试意图被准备工作淹没。fromPartial 用来明确表达这个简化场景。
但它不会自动补出被省略的数据。代码如果实际访问缺失字段,仍可能出错。测试构造方便,不等于有完整运行时校验。
fromAny 则用于故意传错误输入,例如传数字 id,检查函数会不会拒绝。类型断言同样不是数据转换:把数字断言成字符串,不会改变真实运行中的数值。
所以作者限制只在测试中使用。迁移时仍需看每条测试的目的,不能机械替换所有 as。这里的“类型更安全”需要按函数用途理解,尤其不能把 fromAny 当成过滤错误数据的工具。
将 as Type 替换为 fromPartial()
迁移前:
getUser({ body: { id: "123" } } as Request);
迁移后:
import { fromPartial } from "@total-typescript/shoehorn";
getUser(fromPartial({ body: { id: "123" } }));
as Type → fromPartial()
Before:
getUser({ body: { id: "123" } } as Request);
After:
import { fromPartial } from "@total-typescript/shoehorn";
getUser(fromPartial({ body: { id: "123" } }));
将 as unknown as Type 替换为 fromAny()
迁移前,这里故意把应为字符串的 id 写成数字,用来验证错误输入:
getUser({ body: { id: 123 } } as unknown as Request); // wrong type on purpose
迁移后:
import { fromAny } from "@total-typescript/shoehorn";
getUser(fromAny({ body: { id: 123 } }));
as unknown as Type → fromAny()
Before:
getUser({ body: { id: 123 } } as unknown as Request); // wrong type on purpose
After:
import { fromAny } from "@total-typescript/shoehorn";
getUser(fromAny({ body: { id: 123 } }));
什么时候选择哪个函数
| 函数 | 用途 |
|---|---|
fromPartial() |
提供部分数据,并检查已提供部分的类型 |
fromAny() |
故意提供错误类型的数据,同时保留自动补全便利 |
fromExact() |
要求完整对象,后续可按需要换成 fromPartial |
When to use each
| 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) |
执行流程
- 了解需求:询问哪些测试中的
as带来问题;是否只需要大对象的一小部分;是否需要故意传入错误数据来测试异常处理。 - 安装并逐项迁移:
- [ ] 执行
npm i @total-typescript/shoehorn。 - [ ] 用
grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts"找到测试中的断言。 - [ ] 将适用的
as Type换为fromPartial()。 - [ ] 将适用的双重断言换为
fromAny()。 - [ ] 添加相应导入。
- [ ] 运行类型检查验证结果。
- [ ] 执行
Workflow
-
Gather requirements - ask user:
- What test files have
asassertions causing problems? - Are they dealing with large objects where only some properties matter?
- Do they need to pass intentionally wrong data for error testing?
- What test files have
-
Install and migrate:
- [ ] Install:
npm i @total-typescript/shoehorn - [ ] Find test files with
asassertions:grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts" - [ ] Replace
as TypewithfromPartial() - [ ] Replace
as unknown as TypewithfromAny() - [ ] Add imports from
@total-typescript/shoehorn - [ ] Run type check to verify
- [ ] Install:
迁移后仍需确认测试真的运行了目标路径
查找 as 断言只能得到候选,不能不看上下文地全局替换。某些断言有不同用途,测试实际访问的字段也可能比你以为的更多。
安装、改写、补导入、运行类型检查,是原文规定的迁移步骤。类型检查通过不代表测试行为正确,尤其是部分数据被代码访问到缺失字段时,运行仍可能失败。
这项技能非常专项。你现在重点理解类型系统和测试样本的关系即可,不需要为了学完目录而给任何项目强行安装该库。
先作答,再看参考思路
用自己的话说明:它解决什么问题,完成后会留下什么?
请各用一句话回答。若它只做规划或解释,不要把“已开发”“已部署”写成产物。
fromPartial 能保证对象在运行时拥有所有字段吗?
我已思考,查看参考思路
不能。它方便表达部分测试数据;被测代码访问缺失字段仍可能出错。要确保省略部分与当前测试目的相容。
原文中哪条要求在你的环境下可能不成立?
说出具体一句及其前提,例如工具不可用、资料缺失、已有项目约定冲突,或它只是作者偏好。把你的答案带回课堂,我们据此继续讨论。
把方法放进一个具体情境
教学案例:测试解析器拒绝数值形式的文章标题,应清楚标记这是故意的非法输入,不能把同样绕过类型的手法带进生产路径。
边界与容易误读的地方
这是测试辅助工具,不是运行时数据验证器。改写语法不等于测试更真实,也不要在不使用 TypeScript 的项目强加它。
讨论后再实践:先判断上述情境是否适用,再选择真实任务。现在无需安装、运行命令或修改现有项目。