再学 AI
第 36 课 / migrate-to-shoehorn
专项工具阅读 → 对照 → 问答 → 场景

在 TypeScript 测试里表达局部或错误输入

这是作者特定技术栈的专项迁移工具,用于减少测试中随意的类型断言。

还不清楚 Skill、Agent、安装和调用?先读 从零开始的6节入门课

在关系图中查看 migrate-to-shoehorn 与其他技能的关联 →

先把必要的概念讲清楚

这课涉及 TypeScript 测试数据。核心困难是测试往往只关心大对象的少数字段,却被类型要求迫使构造大量无关数据。

下面是老师补充的入门说明;原作者的要求保留在中英对照正文中。所有例子均为帮助理解而构造的教学情境。

package / dependency|代码包与依赖

package 是可以统一安装、导入或管理的一组代码;dependency 是当前程序需要的其他代码。开发依赖常用于检查和构建。安装了一个包只说明代码可取得,还要配置并调用,才会实际参与工作。

fixture / harness|测试样本与运行装置

fixture 是为复现或测试准备的已知输入和初始数据,例如固定课程清单。harness 是把代码、输入和检查串起来的运行装置,例如一次命令启动最小服务并断言结果。它们帮助每次在相同条件下比较,而不是凭上次页面看起来怎样判断。

mock|替代真实依赖的测试对象

测试时用可控制的对象替代真实服务,例如让模型调用固定返回一句答案。这样可以稳定检查程序如何处理响应,但无法据此证明真实模型总能生成好答案。过度替代内部组件还会让测试和内部写法绑定,一重构就要改测试。

interface / API|接口

使用者与一项能力交互时遵循的约定,包括可调用什么、传入什么、得到什么、失败怎样表示。接口可以是程序函数,也可以是网络请求。创建收藏接口接收用户和课程信息、返回收藏结果;它不需要向调用者暴露数据库表结构。这里的接口通常不是指页面外观。 本教材涉及更广义的“接口”时,还包括错误、调用顺序和约束等使用约定;不能把接口一概等同于网络 API。

读原文,理解每一步为什么这样做

左右内容按小节对应;窄屏先中文、后英文。两种语言均完整展示,对应讲解紧接在小节之后。译文传达原文要求;老师讲解补充概念、原因、例子与适用边界。

中文译文English · 英文原文
中文译文
name: migrate-to-shoehorn
description: "把测试中的 as 类型断言迁移到 @total-typescript/shoehorn。用户提到 shoehorn、希望替换测试中的 as,或需要部分测试数据时使用。"
English · 英文原文
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 改善测试数据的类型写法

English · 英文原文

Migrate to Shoehorn

中文译文

为什么使用 shoehorn

shoehorn 让测试可以传入部分数据,同时让 TypeScript 接受这种构造。作者用它替代一些直接写 as 的类型断言。

只用于测试代码,不要用于正式产品代码。

直接使用 as 的不便包括:开发者通常被建议少用断言;必须手工指定目标类型;为了故意传错误数据,还常要写 as unknown as Type

English · 英文原文

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
English · 英文原文

Install

npm i @total-typescript/shoehorn
中文译文

常见迁移方式

English · 英文原文

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" },
    }),
  );
});
English · 英文原文
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" } }));
English · 英文原文
as TypefromPartial()

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 } }));
English · 英文原文
as unknown as TypefromAny()

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
English · 英文原文

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)
中文译文

执行流程

  1. 了解需求:询问哪些测试中的 as 带来问题;是否只需要大对象的一小部分;是否需要故意传入错误数据来测试异常处理。
  2. 安装并逐项迁移:
    • [ ] 执行 npm i @total-typescript/shoehorn
    • [ ] 用 grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts" 找到测试中的断言。
    • [ ] 将适用的 as Type 换为 fromPartial()
    • [ ] 将适用的双重断言换为 fromAny()
    • [ ] 添加相应导入。
    • [ ] 运行类型检查验证结果。
English · 英文原文

Workflow

  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
老师讲解 · 对应上方原文 · 含教学举例

迁移后仍需确认测试真的运行了目标路径

查找 as 断言只能得到候选,不能不看上下文地全局替换。某些断言有不同用途,测试实际访问的字段也可能比你以为的更多。

安装、改写、补导入、运行类型检查,是原文规定的迁移步骤。类型检查通过不代表测试行为正确,尤其是部分数据被代码访问到缺失字段时,运行仍可能失败。

这项技能非常专项。你现在重点理解类型系统和测试样本的关系即可,不需要为了学完目录而给任何项目强行安装该库。

原作者:Matt Pocock · 中文翻译为非官方译本

来源:skills/misc/migrate-to-shoehorn/SKILL.md ↗

固定版本:3cca18b368ae95cdbdebbff572ccafa662551015

先作答,再看参考思路

Q1 · 理解

用自己的话说明:它解决什么问题,完成后会留下什么?

请各用一句话回答。若它只做规划或解释,不要把“已开发”“已部署”写成产物。

Q2 · 判断

fromPartial 能保证对象在运行时拥有所有字段吗?

我已思考,查看参考思路

不能。它方便表达部分测试数据;被测代码访问缺失字段仍可能出错。要确保省略部分与当前测试目的相容。

Q3 · 追问

原文中哪条要求在你的环境下可能不成立?

说出具体一句及其前提,例如工具不可用、资料缺失、已有项目约定冲突,或它只是作者偏好。把你的答案带回课堂,我们据此继续讨论。

课堂回传格式:第 36 课 / 我的理解 / Q2 回答 / 仍不理解的原句。这里是阅读教材;实时问答在我们的对话中进行。

把方法放进一个具体情境

教学案例:测试解析器拒绝数值形式的文章标题,应清楚标记这是故意的非法输入,不能把同样绕过类型的手法带进生产路径。

边界与容易误读的地方

这是测试辅助工具,不是运行时数据验证器。改写语法不等于测试更真实,也不要在不使用 TypeScript 的项目强加它。

讨论后再实践:先判断上述情境是否适用,再选择真实任务。现在无需安装、运行命令或修改现有项目。

关联阅读