关于 prinfer
typeprobe 将 TypeScript 编译器结果提供给编码 Agent 和测试,包括推断类型、补全与诊断。它提供 MCP 服务、快照测试 API、命令行工具和同步库 API。
主要功能
- 01查看推断类型与符号信息
- 02获取编辑器式补全
- 03报告单文件类型诊断
- 04检查冗余或过宽注解
- 05在测试中快照推断类型
- 06提供 MCP 服务、命令行与库 API
适用场景
- 01编辑代码时核对推断类型
- 02修改文件后检查类型错误
- 03通过快照发现 API 类型变化
MCP 相关
typeprobe 以 stdio MCP 服务运行,供编码 Agent 连接。其工具支持类型悬停、批量查询、补全、诊断和注解检查。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y prinferPRINFER_BACKENDBackend for the hover and diagnostics tools. Use typescript6 if the native TypeScript 7 backend fails in your project.PRINFER_COMPILERWhich TypeScript compilers to use: prinfer's bundled ones, the project's own (typescript 5.0 to 6.x; typescript 7 or @typescript/native-preview), or auto: the project's when supported, else bundled.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
6收录的工具列表 · 更新于 3 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | string | 是 | TS/JS 文件:绝对路径,或相对于服务器当前工作目录的路径 |
| full | boolean | 否 | 未截断的类型(不含 "... 12 more ..."),包含所有重载 |
| line | integer | 否 | 从 1 开始计数的行号,用于在同名项重复时选择 |
| name | string | 是 | 符号名称,例如 "createHandler" |
| backend | enum | 否 | 默认使用 typescript7;如果失败或看起来不正确,请重试 typescript6typescript6 | typescript7 |
| project | string | 否 | tsconfig.json;默认:文件上方最近的配置文件 |
| max_chars | integer | 否 | 每种类型的文本上限(默认 4000 个字符;0:不限制) |
| include_cost | boolean | 否 | 统计类型实例化次数;仅适用于 typescript6 |
| include_docs | boolean | 否 | 同时返回 JSDoc/TSDoc |
详细介绍
typeprobe
typeprobe 会询问 TypeScript 编译器推断结果(类型、补全和类型错误),让 AI 编码 Agent 和测试不必靠猜。它提供:
- 面向编码 Agent 的 MCP 服务(Claude Code、Codex、Cursor、VS Code、Gemini CLI,以及任何 stdio 客户端),
typeprobe/testing,以字符串形式返回推断类型,用于快照测试,- CLI 和同步库 API。
typeprobe 在 4.0.0 之前名为 prinfer。请参阅从 prinfer 迁移。
快速开始
让你的 Agent 使用编译器:
npx -y typeprobe setup claude # or codex, cursor, vscode, gemini; see Install
现在,Agent 可以调用 hover_by_name(file: "src/utils.ts", name: "names") 并获得 Type: string[],也可以在编辑后调用 diagnostics(file)。
在测试中锁定 API 推断出的类型(Vitest、Jest、Bun,或任何支持快照的运行器):
import { expect, test } from "vitest"; // or "bun:test"
import { inferredType } from "typeprobe/testing";
import { groupBy, type User } from "../src/users";
const users: User[] = [{ name: "Ada", role: "admin" }];
const byRole = groupBy(users, (user) => user.role);
test("groupBy keys the result by the callback's return type", () => {
expect(inferredType(import.meta.url, { name: "byRole" }))
.toMatchInlineSnapshot(`"Record<Role, User[]>"`);
});
将匹配器留空(toMatchInlineSnapshot()),运行器会自动填充。重构导致类型变化时,测试就会失败。
安装
所有命令都通过 npx 运行,因此无需预先安装任何内容。运行 npm i -g typeprobe 后,去掉 npx -y 前缀:此时设置会注册 typeprobe-mcp 二进制文件,从而跳过 npx 每次启动时的包检查。
Claude Code。 插件内置 MCP 服务和一个技能,用于告诉 Claude 何时使用它:
/plugin marketplace add clockblocker/typeprobe
/plugin install typeprobe@typeprobe
其他客户端,或未安装插件的 Claude Code:
| 客户端 | 命令 | 写入内容 |
|---|---|---|
| Claude Code | npx -y typeprobe setup claude | claude mcp add;使用 --scope project 可写入共享的 .mcp.json |
| Codex | npx -y typeprobe setup codex | codex mcp add(~/.codex/config.toml) |
| Cursor | npx -y typeprobe setup cursor | ~/.cursor/mcp.json;使用 --scope project:.cursor/mcp.json |
| VS Code | npx -y typeprobe setup vscode | code --add-mcp;使用 --scope project:.vscode/mcp.json |
| Gemini CLI | npx -y typeprobe setup gemini | ~/.gemini/settings.json;使用 --scope project:.gemini/settings.json |
对于任何其他 MCP 客户端:typeprobe 是一个通过 npx -y typeprobe mcp 启动的 stdio 服务。
{
"mcpServers": {
"typeprobe": { "command": "npx", "args": ["-y", "typeprobe", "mcp"] }
}
}
它也收录在 MCP Registry 中,条目为 io.github.clockblocker/typeprobe,并收录在 Smithery 上。
设置选项:
--print显示命令或配置变更,但不实际应用。- 即使已安装
typeprobe-mcp,--npx也会注册npx -y typeprobe mcp。当客户端找不到typeprobe-mcp时可使用此选项;从 shell 外部启动的编辑器通常无法找到 nvm、fnm 或 volta 路径。 - 重新运行设置会更新
typeprobe条目的命令,并保留其他服务。JSON 配置会保留你添加到该条目的键,例如env。如果 JSON 配置无法解析,则不会修改,设置会打印需要手动添加的条目。 - 重命名前的
prinfer条目会被替换,而不会与新条目并存:claude和codex会将其删除;JSON 配置会原位将其替换为typeprobe,并保留其他键(将PRINFER_*环境变量名称改为TYPEPROBE_*)。 - 在 Windows 上,服务会注册为
cmd /c npx -y typeprobe mcp(或cmd /c typeprobe-mcp),因为客户端启动服务时不会使用 shell,无法运行 npm 的.cmd启动脚本。
告诉 Agent 何时使用它
工具描述的作用有限;Agent 仍会优先使用 tsc 或手动编写类型注解。在你的指令文件中添加一段简短的用法说明(Claude Code 插件通过技能提供了相同的指导):
npx -y typeprobe setup agents-md # ./AGENTS.md
npx -y typeprobe setup agents-md --file CLAUDE.md
该段内容位于 和 之间;重新运行会原位更新它,并替换 prinfer 写入的段落(``)。
MCP 工具
| 工具 | 用途 |
|---|---|
hover_by_name | 查询具名符号的类型。从这里开始。 |
hover | 查询某行中某个标记的类型,例如回调参数、表达式和重复名称。 |
batch_hover | 一次调用最多查询 100 项,可跨文件。 |
completions | 查看 TypeScript 在光标处提供的内容,例如字符串字面量联合成员。 |
diagnostics | 检查单个文件中的类型错误,无需检查整个项目。 |
annotations | 查看 TypeScript 本来就会推断出的注解。 |
共享参数:
file是绝对路径,或相对于服务工作目录的路径。行和列从 1 开始计数。project是tsconfig.json路径;默认使用文件上方最近的配置。backend(默认为typescript7,也可以是typescript6),适用于悬停工具和diagnostics。如果调用失败,或在 TypeScript 7 上的结果看起来不正确,请改用typescript6重试。请参阅后端和编译器。- 悬停工具接受
include_docs(JSDoc)、include_cost(请参阅类型成本预算)、full和max_chars。每种类型的文本上限为 4000 个字符;被截断的类型会以类似… truncated: 187 chars total, union of 4 members. Pass max_chars: N to see more (0 for no limit).的行结尾。full: true会关闭 TypeScript 自身的截断({ ...; }、... 12 more ...),并列出所有重载。结构化内容永远不会被截断。
hover_by_name
hover_by_name(file: "src/utils.ts", name: "names")
hover_by_name(file: "src/utils.ts", name: "names", line: 11) # line picks among same-named symbols
Type: string[]
Name: names
Kind: const
Position: 11:14
当某个名称声明了多次时,查询会优先选择声明,并说明选中了哪一个:Matched line 8 of 2 declarations (also 12); pass line to choose. 结构化结果会将其他声明列为 alternatives: [{ line, column, kind }]。如果 line 与任何声明都不匹配,则会失败并返回 SYMBOL_NOT_FOUND,同时在 declaredAt 中列出声明所在的行。如果 line 指向的是名称的使用处而非声明处,例如 if 中的 box.value,则会返回该处的类型,并根据其周围的代码进行缩窄。
对于重载函数,结果会显示第一个签名,之后最多再显示三个(full: true 会列出所有签名;结构化的 overloads 始终包含全部签名):
Type: (value: string): number (+1 overload)
Overloads:
(value: number): string
Returns: number
Name: parse
Kind: function
Position: 1:17
hover
传入从该行复制的 text,typeprobe 会查找列位置。会优先匹配完整标识符:对于 users.map((user) => user.name),text: "user" 会跳过 users 并选中回调参数;occurrence: 2 会选中下一个 user。只有当该行没有完整标识符匹配项时,才会将 text 作为子字符串匹配。也可以用 column 代替 text。泛型调用会像在编辑器中一样显示实例化后的类型。
hover(file: "src/utils.ts", line: 11, text: "user")
Type: { id: number; name: string; }
Name: user
Kind: parameter
Position: 11:33
Target: "user" at 11:33
如果该行中没有相应文本,错误信息会引用该行:
Error [SYMBOL_NOT_FOUND]: Text "nope" not found on line 11 of /project/src/utils.ts
Nearby identifiers: name, names, map, user, users
Suggestion: Line 11 reads: "export const names = users.map((user) => user.name);". Copy text exactly from it, or give a column instead.
batch_hover
每个项目可以是 {name, line?}、{line, text, occurrence?} 或 {line, column},也可以带有可选的 file,用于覆盖共享的顶层 file。每个项目的失败会单独处理,包括文件不存在的情况,因此某次查询失败不会丢失其余结果。
batch_hover(file: "src/utils.ts", positions: [
{name: "users"},
{line: 11, text: "user"},
{file: "src/missing.ts", line: 1, column: 1}
])
Batch hover results: 2 succeeded, 1 failed
--- src/utils.ts:users:6 ---
Type: { id: number; name: string; }[]
...
--- src/missing.ts:1:1 ---
Error [FILE_NOT_FOUND]: File not found: /project/src/missing.ts
Suggestion: Check the path. Relative paths resolve against the MCP server's working directory (/project); pass an absolute path to be sure.
行或列超出文件范围时,会返回 INVALID_ARGUMENT 错误,并给出有效范围。
悬停结果
每次悬停在每个后端和界面上都会返回相同的字段:
signature:单独一行的类型文本,不含编辑器悬停内容开头的声明关键字或名称。变量为string[],函数为(value: string): number,调用则为实例化后的签名。类型别名保留其名称和类型参数(type Event = { kind: "open"; ... } | ...);接口和类则显示其名称(Box<T>)。可选成员的写法与tsc在声明中的写法一致:digits?: number;仅当源代码中写了| undefined,或类型并非来自注解(如实例化的泛型、Partial<T>)时,才会包含| undefined。display:编辑器的悬停文本(const names: string[])。只有 TypeScript 7 语言服务器会报告此项。kind:编辑器显示的标签(function、method、const、parameter、property、type、interface、class、enum等);若为调用表达式的被调用对象,还会包含call。- 适用时还会返回
returnType、documentation、overloads、unionMembers和alternatives。 cost:启用include_cost: true时返回{ instantiations, types }。compiler:负责响应的编译器{ name, version, source }。
补全
光标位于 column 指定字符之前;对于字符串联合类型,将光标放在起始引号内侧。
completions(file: "src/utils.ts", line: 14, column: 30)
completions(file: "src/utils.ts", line: 20, column: 1, prefix: "use", limit: 20)
条目按 TypeScript 的排序规则排列:先是局部项、成员和字面值,然后是全局项(同一排序级别的其他条目之后才是关键字),最后是自动导入。最多返回 limit 个条目(默认值为 50,最高为 500);如果匹配项更多,文本会以 … 947 more; pass prefix to narrow, or raise limit 结尾,结构化结果则包含 total 和 truncated: true。
prefix 会保留以其开头的名称,且不区分大小写。未设置时,光标左侧输入的文本会像在编辑器中一样筛选列表(在 useSt 中的 use 后,只显示 use… 名称);prefix: "" 可关闭此筛选。在接受任意键的对象字面量键处(Record<string, number>),typeprobe 不会返回条目,而是返回说明这一情况的 note,而不是列出所有全局项。completions 始终在 TypeScript 6 上运行。
诊断
对你编辑的每个文件运行此工具;当没有报告错误时,编辑就完成了。
diagnostics(file: "src/utils.ts")
diagnostics(file: "src/utils.ts", include_suggestions: true) # also TS6133 unused variables etc.
src/utils.ts:16:14 error TS2322: Type 'string' is not assignable to type 'number'.
1 error, 0 warnings.
没有问题的文件会返回 No type errors.。
注解
annotations(file: "src/utils.ts")
src/utils.ts:2:55 redundant return type of format: declared string, inferred string (exported)
src/utils.ts:14:19 widening drink: declared Drink, inferred "tea" (exported)
2 redundant, 2 widening (5 annotations checked).
redundant: delete the annotation; the type stays the same.
widening: the annotation is wider than the inferred type; keep it if the wider type is intended.
删除 redundant 注解不会改变类型。widening 注解通常是有意为之(例如之后必须能接受任意 Drink 的变量,或导出的契约),因此应将其视为需要确认的问题。只有带有可用于推断的初始化表达式或函数体的注解才会被检查。每条结构化发现都包含 : Type 文本的范围、declared、inferred、exported 和 suggestion。annotations 始终在 TypeScript 6 上运行。
错误
每个工具都会返回文本和结构化内容:{ version: 1, ok: true, result } 或 { version: 1, ok: false, error }。失败文本会包含代码、消息和恢复提示,因为许多客户端只会向模型展示文本:
Error [SYMBOL_NOT_FOUND]: No symbol named "nmes" at line 11 found in src/utils.ts
Did you mean: names, name?
Suggestion: Check the spelling against candidates, pass line to pick the match on a known line, or call hover with that line and text copied from it.
error 包含 code(INVALID_ARGUMENT、FILE_NOT_FOUND、SYMBOL_NOT_FOUND、TYPESCRIPT_ERROR 或 INTERNAL_ERROR)、message、suggestion,以及适用时的 file、line、column、project、candidates(文件中相近的标识符,或该行附近的标识符)和 declaredAt。每个工具的 outputSchema 都会在同一封装中覆盖成功和失败两种结果,并列出 Agent 可据以恢复的错误字段;其他字段也仍会发送。CLI 的 --json 输出以及导出的 zod 模式(hoverSuccessSchema、contractErrorResponseSchema 等)使用相同的契约。
类型测试
typeprobe/testing 会返回编辑器显示的类型,因此快照无需手动编写,不同于 expectTypeOf 或 tsd;任何更改都会显示出来,包括字面量联合类型扩大为 string。有意更改后,使用运行器的更新标志更新快照(vitest -u、bun test --update-snapshots)。两种后端上的查找辅助函数都是同步的,因此不用担心忘记等待 Promise。
import { inferredCompletions, inferredType } from "typeprobe/testing";
// Another module: resolve it against the test file.
expect(inferredType(new URL("../src/users.ts", import.meta.url), { name: "groupBy" }))
.toMatchInlineSnapshot(`"<T, K extends PropertyKey>(items: readonly T[], key: (item: T) => K): Record<K, T[]>"`);
// Line 7 of this test file reads: const fallback: Role = "member";
expect(inferredCompletions(import.meta.url, { line: 7, text: '"' }))
.toMatchInlineSnapshot(`
[
"admin",
"member",
]
`);
| 辅助函数 | 返回值 |
|---|---|
inferredType(file, selector) | 打印出的类型(signature)。 |
inferredTypeInfo(file, selector) | 完整的悬停结果:名称、种类、返回类型、文档、成本。 |
inferredCompletions(file, selector) | 在 TypeScript 7 上,返回光标处的所有补全名称,不进行筛选或限制。 |
inferredTypeCost(file, selector) | { instantiations, types };参见类型成本预算。 |
inferredTypeIssues(file, selector) | 可读性问题;参见可读性检查。 |
typeReadabilityIssues(text, rules?) | 任意打印出的类型文本中的可读性问题。 |
expectType(file, selector) | 一次性检查文本、成本和可读性;参见 expectType。 |
expectTypes(file, selector) | 对多个目标调用 expectType,并为它们整体设置预算;参见 expectTypes。 |
closeTestingSessions() | 提前停止 TypeScript 7 编译器;参见测试中的 TypeScript 7。 |
文件参数可以是测试文件本身的 import.meta.url,也可以是相对于它解析的 URL。普通字符串路径则相对于 process.cwd() 解析。文件会通过离它最近的 tsconfig.json 或 project 读取。
选择器用于选定目标:
{ name, line? }:按名称查找声明;line用于从重复项中选定一个。{ line, text, occurrence? }:匹配该行上text起始处的词元,匹配方式与hover工具的text相同。对于inferredCompletions,光标位于匹配内容之后,因此text: "user."会列出成员,而text: '"'会列出字符串字面量成员;cursor: "start"会将光标置于匹配内容之前。{ line, column }:从 1 开始计数的位置(对于补全,光标位于该列之前)。
选项放在同一个对象中,例如 { name: "byRole", backend: "typescript7" }。传入第三个参数会抛出异常。
| 选项 | 默认值 | 作用 |
|---|---|---|
backend | "typescript6" | "typescript7" 会打印 TypeScript 7 的输出。inferredCompletions 始终使用 TypeScript 7。 |
full | true | false 会返回编辑器的缩略形式({ ...; })。 |
sort_unions | false | 按固定顺序打印联合类型;见下文。 |
include_docs | false | 在 inferredTypeInfo 中添加 documentation。 |
include_cost | false | 在 inferredTypeInfo 中添加 cost(仅限 TypeScript 6)。 |
project | 最近的 tsconfig.json | 读取文件时使用的 tsconfig。 |
compiler | TYPEPROBE_COMPILER,否则为 "bundled" | 要使用哪个 TypeScript;参见内置编译器或项目编译器。 |
timeout | 60000 | TypeScript 7 调用抛出异常前可阻塞的毫秒数。 |
strict | false | 遇到未知键时抛出异常,并指出最接近的有效键。测试运行器不会对测试文件进行类型检查,因此若不启用此选项,驼峰式 sortUnions 不会产生任何效果。 |
查找失败时会抛出异常,并在消息中提供修正建议:未知名称会列出最接近的声明,找不到文本时会引用该行,找不到相对路径时会说明如何相对于测试文件解析该路径。
typeprobe/vitest 是 typeprobe/testing 的弃用别名。
稳定的联合类型顺序
TypeScript 6 和 TypeScript 7 打印联合类型成员的顺序不同(一个打印为 "idle" | "error",另一个打印为 "error" | "idle"),因此在一个后端上编写的快照无法通过另一个后端的检查。sort_unions: true 会让任意深度的所有联合类型都按相同顺序打印:按文本排序成员(基于 UTF-16 代码单元,因此顺序为 1 | 10 | 2),并将 null 和 undefined 放在最后。
expect(inferredType(import.meta.url, { name: "status", sort_unions: true }))
.toMatchInlineSnapshot(`""error" | "idle" | "loading" | null"`);
只有联合类型成员的顺序会改变。属性顺序仍可能不同:cond ? { ok: true, value: 1 } : { ok: false, error: "e" } 在 TypeScript 6 中打印为 { ok: false; error: string; value?: undefined; },在 TypeScript 7 中则打印为 { value?: undefined; ok: false; error: string; };对联合键进行映射的类型也会按各后端中的联合类型顺序列出属性。该选项也可用于库的悬停选项和 CLI(--sort-unions)。
没有值的类型
若要固定一个函数为没有实际值的参数推断出的类型,请在 fixture 文件中声明该参数。在测试文件本身中使用 declare const 不会产生运行时值,因此测试运行时会抛出 ReferenceError。
// test/groupBy.fixture.ts, read for types only, never run
import { groupBy, type User } from "../src/users";
declare const input: User[];
export const byName = groupBy(input, (user) => user.name);
// test/users.test.ts
expect(inferredType(new URL("./groupBy.fixture.ts", import.meta.url), { name: "byName" }))
.toMatchInlineSnapshot(`"Record<string, User[]>"`);
可读性检查
类型本身可能正确,但打印出来的形式仍需要读者费心解读。inferredTypeIssues(file, selector) 会返回目标打印类型中存在此类问题的位置(空数组表示没有问题);typeReadabilityIssues(text, rules?) 则可检查任意类型文本。默认规则如下:
utility-type:带有类型参数、尚未解析的工具类型,深度不限:Omit、Pick、Partial、Required、Readonly、Exclude、Extract、NonNullable、ReturnType、Parameters、ConstructorParameters、InstanceType、Awaited、ThisParameterType、OmitThisParameter(导出为DEFAULT_UTILITY_TYPES)。嵌套工具类型只报告最外层的一个。对于打印签名的类型参数上的工具类型(例如<T>(value: T) => Omit<T, "id">中的Omit<T, "id">),不会报告,因为在T已知之前无法解析它。Record、Promise、Array等其他泛型类型不会被标记。object-intersection:与对象类型相交的类型,例如User & { id: string; }或映射类型。保留字面量建议的string & {}不会被标记。truncation:... 3 more ...、{ ...; }、...占位符,以及达到长度限制后被截断的文本。除非设置full: false,否则这些辅助函数会打印未截断的类型,因此此规则主要用于捕获full: false和来自其他位置的文本。
文本会使用 TypeScript 扫描器和解析器读取,因此字符串字面量类型(如 "Omit<" 或 "...")不会被标记,剩余参数或展开元组也不会被标记。每个问题的格式为 { rule, text, offset, message }。即使移除截断标记后仍无法解析的文本(因达到长度限制而被截断)只会产生截断问题。
rules 可设置 utilityTypes(要标记的名称,例如 [...DEFAULT_UTILITY_TYPES, "DeepPartial"],或 false)、objectIntersections 和 truncation(设为 false 可关闭相应规则),以及 allow(允许按当前形式打印的片段):
expect(
inferredTypeIssues(import.meta.url, { name: "userId", rules: { allow: ['string & { readonly __brand: "UserId"; }'] } }),
).toEqual([]);
类型成本预算
inferredTypeCost 会统计 TypeScript 为某个类型所做的工作,因此测试可以防止开销大的类型变得更加昂贵:
import { inferredTypeCost } from "typeprobe/testing";
test("userSchema stays cheap to infer", () => {
expect(inferredTypeCost(import.meta.url, { name: "userSchema" }).instantiations)
.toBeLessThan(5_000);
});
它会返回 { instantiations, types }:类型实例化次数(即 tsc --extendedDiagnostics 报告的 Instantiations),以及全新的 TypeScript 6 检查器在解析目标并输出未截断类型时创建的类型。由于每项计数都会使用新的检查器,因此无论运行多少次、在哪个进程中运行、测试顺序如何,或使用何种显示选项,数字都相同;只有代码、编译器选项和 TypeScript 版本会影响它们。除非传入 compiler: "project",否则计数会使用 typeprobe 捆绑的 TypeScript 6;无论采用哪种方式,在对类型设置预算的项目中都应固定该版本。成本对象上不可枚举的 compiler 属性会标明用于计数的版本。typeprobe 不报告运行耗时,因为相同的运行耗时可能相差数倍。
计数涵盖目标类型所需的工作:对于 const x = expr,会检查 expr;对于没有返回类型注解的函数,会推断其返回类型。不会计入类型不依赖的代码,例如带注解的初始化表达式(const x: T = expr 的类型取自 T);要对某个表达式计数,请使用 { line, text } 指定目标。
要对同一文件中的多个目标计数,请传入 names,按名称返回记录;或者传入 targets(任意选择器形式,不带选项),按顺序返回数组。只有 project、compiler 和 strict 可以与它们并列;使用 strict: true 时,批量调用会拒绝 full 等显示选项。
const costs = inferredTypeCost(import.meta.url, { names: ["userSchema", "orderSchema"] });
expect(costs.userSchema.instantiations).toBeLessThan(5_000);
const [first, second] = inferredTypeCost(import.meta.url, {
targets: [{ name: "userSchema" }, { line: 12, text: "parse(" }],
});
批量调用的计数结果与单次调用完全一致。它们只共享加载过程:在同一进程中,项目文件按编译器解析一次,因此在项目的另一个文件中计数,只需在计数耗时之外再花 30 到 70 毫秒,而不是大约 300 毫秒(以 typeprobe 自身的代码库为例);未更改的目标也只会计数一次。计数本身不会共享:计数一个需要 80,000 次实例化的类型约需 250 毫秒。bun test 会在一个进程中运行所有测试文件,因此项目只加载一次;使用 Vitest 默认的隔离设置时,每个测试文件都会重新加载项目。
TypeScript 7 不会公开实例化次数,因此成本计数仅支持 TypeScript 6:使用 backend: "typescript7" 时,inferredTypeCost 和 include_cost 会抛出错误。在其他情况下,include_cost(MCP、库)和 --cost(CLI)会将相同的数字添加到悬停结果中。
如果使用 TypeScript 7 进行类型检查,TypeScript 6 的计数仍可作为接近的参考。使用 --extendedDiagnostics 测量(TypeScript 7.0.2 搭配 --checkers 1,对比 TypeScript 6.0.3),在 10 个仅含单个声明的项目中,有 7 个的实例化次数相同,2 个的差异不超过 2%,而一个递归点分路径类型在 TypeScript 7 上的计数高出 17%。按成本排序后,这些声明的顺序几乎相同。在所有测量案例中,预留 20% 的余量都足以满足预算;对于递归键类型和路径类型,请预留更多。请使用 --checkers 1 与 TypeScript 7 进行比较,否则它会累加并行检查器的计数。
expectType
expectType 可在一次调用中检查目标的打印类型、成本和可读性,并抛出一个列出所有未通过检查的错误:
import { expectType } from "typeprobe/testing";
test("user stays readable and cheap", () => {
expectType(import.meta.url, {
name: "user",
printed: "{ id: string; name: string; }",
maxInstantiations: 500,
readable: true,
});
});
expectType failed 3 checks for "user" at test/users.test.ts:9:14:
- printed: the type differs at character 1.
expected: { id: string; name: string; }
actual: Omit<User, "email">
^
- maxInstantiations: 612 instantiations, over the budget of 500 by 112 (counted and printed on typescript 6.0.3, bundled).
- readable: 1 readability issue:
utility-type: Omit<User, "email"> is unresolved: TypeScript printed Omit<...> instead of the type it produces.
选择器是 inferredType 的选项,且至少需要一项检查:
printed:inferredType对同一选择器返回的精确文本(backend、compiler、sort_unions和full均适用)。maxInstantiations、maxTypes:成本预算。即使设置了backend: "typescript7",成本也始终由 TypeScript 6 统计;该选项只决定文本的来源。设置backend: "typescript7"和compiler: "project"时,如果项目中有 TypeScript 6,则使用项目中的版本统计,否则使用捆绑的版本。readable:true表示使用默认的可读性规则,也可以是规则对象。
costCompiler("bundled"、"project" 或 "auto",默认为 compiler 的值)用于选择统计成本的 TypeScript 6,因此文本和计数可能来自不同的编译器:{ backend: "typescript7", costCompiler: "project" } 会使用捆绑的 TypeScript 7 打印文本,并使用项目中的 TypeScript 6 统计成本。预算未通过时,错误会指出两者。inferredTypeCost 只统计成本,因此在该函数中,compiler 是用于统计的编译器,而 costCompiler 会抛出错误。
错误类型为 TypeExpectationError,其中 failures 包含所有未通过的检查;如果文本不一致,还会包含 actual 和 expected,以便 Vitest 和 Jest 打印差异。所有检查通过时,expectType 返回 { printed, cost? }。
expectTypes
expectTypes 会对同一文件中的多个目标运行 expectType,并使用一个涵盖所有目标的预算;若有检查未通过,则抛出一个列出所有未通过检查的错误:
expectTypes(import.meta.url, {
types: [
{ name: "userSchema", maxInstantiations: 3_000 },
{ name: "User", printed: "{ id: string; name: string; }" },
{ name: "orderSchema" },
],
maxInstantiations: 8_000,
backend: "typescript7",
costCompiler: "project",
});
组的 maxInstantiations 和 maxTypes 会使用一个全新的 TypeScript 6 检查器解析每个目标来统计,因此目标共享的工作(例如它们都会访问的架构)只计数一次,就像对模块进行类型检查时一样。检查器统计的数量可能会因遇到类型的顺序而略有不同,因此始终按源码顺序解析目标:无论 types 的顺序如何,总数都相同。每个条目自己的预算仍单独统计;组预算未通过时,会列出每个条目单独统计的数量,而这些数量之和会大于总数。
每个条目都采用 expectType 的选择器;只有当组没有预算时,条目才必须自行指定一项检查。组中的 backend、full、sort_unions、readable 和 timeout 会作为每个条目的默认值。project、compiler 和 costCompiler 只能设置在组上,因为总数是在一个程序中统计的。错误会包含条目的 index;组预算错误则不包含。它返回 { types, cost? }:每个条目的 expectType 结果(如果组有预算,则包含该条目自己的成本)以及组的成本。
在测试中使用 TypeScript 7
TypeScript 7 在工作线程中运行,每个项目使用一个编译器进程,并且每次调用都会阻塞,直到编译器返回结果。两者都不会让测试进程继续运行,因此无需清理。
测试运行器的超时无法中断阻塞的调用,因此 typeprobe 有自己的超时:如果调用在 timeout 毫秒内(默认 60000)未收到答复,就会抛出错误并停止该编译器;下一次调用会启动一个新的编译器。编译器卡住时,只会导致一个测试失败,而不会让整个测试运行停滞。对于加载较慢的项目,可通过共享选择器在每次调用中延长超时时间,例如 const ts7 = { backend: "typescript7", timeout: 120_000 } as const。如果编译器进程退出,正在进行的调用会重试一次。
closeTestingSessions() 会提前停止编译器,也可以安全地不调用。如果要调用,应在每次运行的最后一个测试之后调用一次,而不是每个测试文件调用一次:下一次 TypeScript 7 调用会启动新的编译器并重新加载项目(在 typeprobe 的代码库中约需 190 ms,而热调用只需 2 ms)。使用 bun test 时,将 afterAll(closeTestingSessions) 放在 --preload 文件中(bunfig.toml 中的 preload)。使用 Vitest 时,不要调用:setupFiles 会对每个测试文件运行,而 globalSetup 则会在没有会话的进程中运行。这不会影响 TypeScript 6,因为它的程序会保持加载状态。
命令行
# Type by name, optionally with a line hint
typeprobe src/utils.ts:format
typeprobe src/utils.ts:names:11
typeprobe 'src/store.ts:$store' # any JavaScript identifier
# Type of a token: text copied from the line, or a column
typeprobe src/utils.ts:11:user
typeprobe src/utils.ts:11 --text user --occurrence 2
typeprobe src/utils.ts:11:33
typeprobe src/utils.ts:format --docs
typeprobe src/utils.ts:largeType --full --max-chars 0
typeprobe src/utils.ts:status --sort-unions
typeprobe src/utils.ts:format --cost
typeprobe src/utils.ts:format -p ./tsconfig.json --compiler project
# Completions: the top 50, filtered by the text left of the cursor
typeprobe complete src/utils.ts:14:30
typeprobe complete src/utils.ts:11:user. # cursor right after the text
typeprobe complete src/utils.ts:20:1 --prefix use --limit 20
typeprobe check src/utils.ts # type errors in one file
typeprobe annotations src/utils.ts # annotations TypeScript would infer anyway
$ typeprobe src/utils.ts:format --docs
(value: number, digits?: number): string
returns: string
name: format
kind: function
docs: Formats a number with a fixed number of digits.
类型查询和 check 使用 TypeScript 6,除非传入 --backend typescript7;complete 和 annotations 始终使用 TypeScript 6。运行 typeprobe --help 查看所有选项。
- 在
<file>:<line>:<text>中,行号之后的所有内容都是文本,包括冒号和句点。纯数字文本会被视为列号,因此请通过--text传入。 - 选项值可以使用
--opt value或--opt=value。未知选项或命令不接受的选项都会报错。 - 参数中含有
$时请使用单引号括起来:shell 会展开$store,而 zsh 会将"$F:root"中的$F:r识别为修饰符。 - 失败时,会在 stderr 上打印错误、
Did you mean: …?候选项和建议,并以状态码 1 退出。文件存在类型错误时,check也会以状态码 1 退出(警告不计);annotations无论发现什么都会以状态码 0 退出。 --json会在 stdout 上打印带版本的契约,不会截断,且不会向 stderr 输出任何内容。检查ok,以区分运行失败和文件存在类型错误的情况。
$ typeprobe src/utils.ts:names --json
{"version":1,"ok":true,"result":{"signature":"string[]","line":11,"column":14,"kind":"const","name":"names","compiler":{...}}}
typeprobe mcp 会通过 stdio 启动 MCP 服务,与 typeprobe-mcp 二进制文件的行为相同。typeprobe setup 的说明见 安装。
库 API
该库是同步的,运行于 TypeScript 6,失败时会抛出异常,通常是 TypeprobeError(从 typeprobe 和 typeprobe/testing 导出),其中包含 code 和 suggestion。contractError(error) 会将捕获的错误转换为契约格式。
import { annotations, batchHover, completions, diagnostics, hover } from "typeprobe";
hover("./src/utils.ts", "format");
// => { signature: "(value: number, digits?: number): string", returnType: "string",
// line: 2, column: 17, kind: "function", name: "format" }
hover("./src/utils.ts", "names", { line: 11 }); // line hint for repeated names
hover("./src/utils.ts", 11, 33); // by position
hover("./src/utils.ts", "format", { include_docs: true, include_cost: true });
// Several positions in one file, one program load; bad positions fail per item
batchHover("./src/utils.ts", [{ line: 11, column: 14 }, { line: 11, column: 33 }]);
// => { items: [...], successCount: 2, errorCount: 0 }
// Every entry, ranked, unless you pass prefix or limit
completions("./src/utils.ts", 14, 30).entries.map((entry) => entry.name);
// => ["coffee", "tea"]
diagnostics("./src/utils.ts", { include_suggestions: false });
// => { file, errorCount: 1, warningCount: 0, diagnostics: [{ line: 16, column: 14, code: 2322, ... }] }
annotations("./src/utils.ts").findings.map((finding) => `${finding.kind} ${finding.name}`);
// => ["redundant format", "widening drink", "redundant label", "widening mode"]
选项:project、compiler,以及用于悬停信息的 include_docs、include_cost、full 和 sort_unions。
后端和编译器
| 界面 | 类型查询 | 自动补全 | 类型错误 | 注解 |
|---|---|---|---|---|
| MCP 服务 | TS7;backend: "typescript6" | TS6,前 50 项 | TS7;backend: "typescript6" | TS6 |
| 命令行 | TS6;--backend typescript7 | TS6,前 50 项 | TS6;--backend typescript7 | TS6 |
| 库 | TS6 | TS6,所有条目 | TS6 | TS6 |
typeprobe/testing | TS6;backend: "typescript7" | TS7,所有名称 | 无 | 无 |
typescript7 会运行原生 TypeScript 7 语言服务器,每个项目共享一个预热会话,可供多个请求使用;其输出最接近编辑器中的显示结果。(typeprobe/testing 则使用 TypeScript 7 的编译器 API。)typescript6 会在进程内使用 TypeScript 6 编译器 API。所有位置的类型成本均按 TypeScript 6 计算。
TypeScript 7 后端处于实验阶段:其程序化 API 和悬停格式仍可能发生变化。两个后端都会为名称选择相同的符号、报告名称词元的位置,并按 TypeScript 的方式计算行数(CR、LF、CRLF、U+2028 和 U+2029 均表示行结束;开头的 BOM 会被忽略)。已知差异:
- 只有语言服务器会报告
display。 - 如果语言服务器不会自行选择某个
project(它会使用最近的 tsconfig.json,或该文件引用的项目),例如未被引用的tsconfig.test.json,则会通过 TypeScript 7 API 在同一会话中打开该项目,因此其悬停信息没有display。该 tsconfig 必须通过include、files或导入包含该文件,否则调用会失败并返回INVALID_ARGUMENT。TypeScript 6 会将该文件添加到任何项目中。 - 相对导入所涉及文件的编辑会立即生效。
diagnostics还会重新扫描 tsconfig 的 include 目录(范围有限,会跳过node_modules、构建输出目录和点目录);其他未打开文件的编辑会在稍后通过文件监视器传达给语言服务器。
内置编译器或项目编译器
默认情况下,typeprobe 使用其依赖的 TypeScript 6 和 TypeScript 7(typescript 6,以及 @typescript/native,即 typescript 7 的别名)进行打印和计数,因此无论项目安装什么版本,在每台机器上得到的结果都相同。若要获取你自己的编译器推断结果,让快照和预算随编译器升级而变化,请使用项目编译器:
| 模式 | TypeScript 6 后端 | TypeScript 7 后端 |
|---|---|---|
bundled(默认) | typeprobe 的 typescript | typeprobe 的 @typescript/native |
project | 项目的 typescript,5.0 至 6.x | 项目的 typescript 7、@typescript/native,或 @typescript/native-preview 7.0.0-dev.20260624.1 或更高版本 |
auto | 支持时使用项目版本,否则使用内置版本 | 支持时使用项目版本,否则使用内置版本 |
通过 compiler(库选项和 typeprobe/testing 选择器)、--compiler(CLI)或 TYPEPROBE_COMPILER(所有使用场景,包括 MCP 服务)设置;显式选项优先。软件包的解析方式与 Node 从 tsconfig.json 文件(或 project)所在目录解析导入的方式相同;对于 TypeScript 7,如果 typescript 7、@typescript/native 和 @typescript/native-preview 并列存在,则按此顺序选择最近的一个。typeprobe 会运行该软件包自带的 API 客户端和编译器二进制文件。会将 typeprobe 自身的 typescript 和 @typescript/native 提升到上层目录的软件包管理器,也会把它们放在项目解析它们的位置;只有当项目最近的 package.json 或其工作区根目录的 package.json 声明了该软件包时,这个副本才属于项目,并且此时会作为内置编译器运行和报告。
如果项目没有适用于该后端的编译器,project 会抛出 TYPESCRIPT_ERROR;如果项目使用了不受支持的版本,则会报告软件包名、版本和路径:低于 5.0 的 typescript、被要求输出 TypeScript 6 结果的 typescript 7,或低于 7.0.0-dev.20260624.1 的 @typescript/native-preview。如果项目编译器不受支持,auto 会回退到内置编译器,并在 stderr 上输出一条警告。使用 TypeScript 5.0 至 5.8 时,部分类型和消息的打印结果与 TypeScript 6 不同,因此切换时请预期快照会发生变化。
每个结果都会通过 compiler: { name, version, source } 指明编译器,其中 source 为 "bundled" 或 "project"。在库和测试结果中,它不可枚举,因此仅更换编译器不会导致快照、toEqual 和 JSON.stringify 的结果发生变化;可直接读取它(inferredTypeInfo(...).compiler)。CLI 的 --json 输出和 MCP 结构化内容会在 result 中包含此信息,成本行也会标明编译器:cost: 412 instantiations, 96 types (typescript 6.0.3, bundled)。
一个进程一次只运行一个 TypeScript 7 编译器:需要切换到另一个编译器的调用会等待当前正在执行的调用结束,关闭其会话,然后启动新编译器。
环境变量
| 变量 | 作用 |
|---|---|
TYPEPROBE_BACKEND=typescript6 | 仅适用于 MCP 服务:为悬停工具和 diagnostics 设置默认后端。显式的 backend 参数或 include_cost 优先。 |
TYPEPROBE_COMPILER=project | 所有使用场景均可设为 bundled、project 或 auto;详见上文。 |
prinfer 时代的名称 PRINFER_BACKEND 和 PRINFER_COMPILER 已弃用,但当 TYPEPROBE_* 变量未设置时仍会读取。
要求
Node.js 20 或更高版本。typeprobe 内置了自己的 TypeScript 6 和 7,因此除非选择使用项目编译器,否则项目的 typescript 版本无关紧要。只有在 TypeScript 7 调用需要时,才会加载 TypeScript 7 软件包。
从 prinfer 迁移
prinfer 在 4.0.0 中更名为 typeprobe。除 error.name 现在为 "TypeprobeError" 外,结果、快照和 JSON 契约均未改变。最后一个 prinfer 版本 3.5.0 依赖 typeprobe 并将请求转发给它(包括 prinfer 和 prinfer-mcp 命令,以及 prinfer、prinfer/testing 和 prinfer/vitest 导入),因此旧配置会继续有效,直到你切换。它的 CLI 会在 stderr 上打印一行提示;MCP 服务不会打印任何内容。
| prinfer 3.x | typeprobe 4 |
|---|---|
npm i -D prinfer | npm rm prinfer && npm i -D typeprobe |
import { inferredType } from "prinfer/testing" | import { inferredType } from "typeprobe/testing"(也可使用 typeprobe、typeprobe/vitest) |
prinfer、prinfer-mcp、npx -y prinfer mcp | typeprobe、typeprobe-mcp、npx -y typeprobe mcp |
MCP 服务 prinfer | MCP 服务 typeprobe:重新运行 npx -y typeprobe setup <client>,它会替换 prinfer 条目 |
PRINFER_BACKEND、PRINFER_COMPILER | TYPEPROBE_BACKEND、TYPEPROBE_COMPILER(未设置新名称时仍会读取旧名称) |
错误名称 PrinferError | TypeprobeError,现已导出;PrinferError 是同一类的弃用别名,因此使用任一名称进行 instanceof 检查都有效 |
Claude Code 插件 prinfer@prinfer | /plugin uninstall prinfer@prinfer、/plugin marketplace remove prinfer,然后执行 安装 下的两个命令 |
MCP Registry io.github.clockblocker/prinfer、Smithery clockblocker/prinfer | io.github.clockblocker/typeprobe、clockblocker/typeprobe |
| AGENTS.md 中的 `` 块 | npx -y typeprobe setup agents-md 会替换它 |
用户级别的 setup vscode 会调用 code --add-mcp,该命令无法移除服务:请自行从 VS Code 的 MCP 列表中删除旧的 prinfer 服务。
开发
bun install
bun run ci # typecheck, build, biome check, tests
仓库使用 TypeScript 7 进行类型检查(bun run typecheck);声明文件打包使用 TypeScript 6。
面向用户的更改需添加 changeset(bun run changeset)。发布时,运行 bun run version(它会应用 changeset,并将版本复制到 server.json、插件清单和 MCPB 清单),然后提交,再手动发布:bun run release 会发布到 npm,bun run mcpb 会打包供 Smithery 使用的 bundle。
发布重命名版本(typeprobe 4.0.0 和 prinfer 3.5.0)
shim/prinfer/ 是最后一个 prinfer 版本:纯 JavaScript,无需构建,也不属于 typeprobe 包。只有在 typeprobe 4.0.0 发布到 npm 后才能发布它,因为它依赖 typeprobe@^4.0.0。
- 按常规发布 typeprobe:运行
bun run version,提交chore: release typeprobe 4.0.0,然后运行bun run release(npm),并打上标签v4.0.0。使用npx -y [email protected] --version检查。 - 发布 shim:运行
bun run shim:pack(会生成prinfer-3.5.0.tgz),然后运行npm publish prinfer-3.5.0.tgz。使用npx -y [email protected] --version检查,该命令会打印提示和 4.0.0。 - 弃用所有 prinfer 版本,包括 shim,这样每次安装都会指向 typeprobe(shim 仍可正常工作):
npm deprecate prinfer "prinfer is now typeprobe: npm i -D typeprobe. Migration guide: https://github.com/clockblocker/typeprobe#migrating-from-prinfer" - MCP Registry:运行
mcp-publisher login github、mcp-publisher publish(会注册io.github.clockblocker/typeprobe,注册表会将其与 typeprobe 的 package.json 中的mcpName进行核对),然后弃用旧名称:mcp-publisher status --status deprecated --all-versions \ --message "Renamed to io.github.clockblocker/typeprobe (npm: typeprobe)" \ io.github.clockblocker/prinfer - Smithery:运行
bun run mcpb,然后运行npx -y [email protected] mcp publish ./typeprobe.mcpb -n clockblocker/typeprobe(需要SMITHERY_API_KEY)。在 Smithery 的设置中,将旧的clockblocker/prinfer条目指向新条目,或取消发布该条目。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。