mcpField
P
prinfer

prinfer

本地服务
clockblocker.github.io开发工具13 周前更新

关于 prinfer

typeprobe 将 TypeScript 编译器结果提供给编码 Agent 和测试,包括推断类型、补全与诊断。它提供 MCP 服务、快照测试 API、命令行工具和同步库 API。

主要功能

  1. 01查看推断类型与符号信息
  2. 02获取编辑器式补全
  3. 03报告单文件类型诊断
  4. 04检查冗余或过宽注解
  5. 05在测试中快照推断类型
  6. 06提供 MCP 服务、命令行与库 API

适用场景

  1. 01编辑代码时核对推断类型
  2. 02修改文件后检查类型错误
  3. 03通过快照发现 API 类型变化

MCP 相关

typeprobe 以 stdio MCP 服务运行,供编码 Agent 连接。其工具支持类型悬停、批量查询、补全、诊断和注解检查。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"clockblocker-prinfer": {
"command": "npx",
"args": [
"-y",
"prinfer"
],
"env": {
"PRINFER_BACKEND": "<PRINFER_BACKEND>",
"PRINFER_COMPILER": "<PRINFER_COMPILER>"
}
}
}
}
启动命令npx -y prinfer
需要配置的环境变量
PRINFER_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmprinfer3.4.0

工具

6

收录的工具列表 · 更新于 3 周前

参数类型必填说明
filestring是TS/JS 文件:绝对路径,或相对于服务器当前工作目录的路径
fullboolean否未截断的类型(不含 "... 12 more ..."),包含所有重载
lineinteger否从 1 开始计数的行号,用于在同名项重复时选择
namestring是符号名称,例如 "createHandler"
backendenum否默认使用 typescript7;如果失败或看起来不正确,请重试 typescript6typescript6 | typescript7
projectstring否tsconfig.json;默认:文件上方最近的配置文件
max_charsinteger否每种类型的文本上限(默认 4000 个字符;0:不限制)
include_costboolean否统计类型实例化次数;仅适用于 typescript6
include_docsboolean否同时返回 JSDoc/TSDoc

详细介绍

typeprobe 标志

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 Codenpx -y typeprobe setup claudeclaude mcp add;使用 --scope project 可写入共享的 .mcp.json
Codexnpx -y typeprobe setup codexcodex mcp add(~/.codex/config.toml)
Cursornpx -y typeprobe setup cursor~/.cursor/mcp.json;使用 --scope project:.cursor/mcp.json
VS Codenpx -y typeprobe setup vscodecode --add-mcp;使用 --scope project:.vscode/mcp.json
Gemini CLInpx -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。
fulltruefalse 会返回编辑器的缩略形式({ ...; })。
sort_unionsfalse按固定顺序打印联合类型;见下文。
include_docsfalse在 inferredTypeInfo 中添加 documentation。
include_costfalse在 inferredTypeInfo 中添加 cost(仅限 TypeScript 6)。
project最近的 tsconfig.json读取文件时使用的 tsconfig。
compilerTYPEPROBE_COMPILER,否则为 "bundled"要使用哪个 TypeScript;参见内置编译器或项目编译器。
timeout60000TypeScript 7 调用抛出异常前可阻塞的毫秒数。
strictfalse遇到未知键时抛出异常,并指出最接近的有效键。测试运行器不会对测试文件进行类型检查,因此若不启用此选项,驼峰式 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 typescript7TS6,前 50 项TS6;--backend typescript7TS6
库TS6TS6,所有条目TS6TS6
typeprobe/testingTS6;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 的 typescripttypeprobe 的 @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.xtypeprobe 4
npm i -D prinfernpm 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 mcptypeprobe、typeprobe-mcp、npx -y typeprobe mcp
MCP 服务 prinferMCP 服务 typeprobe:重新运行 npx -y typeprobe setup <client>,它会替换 prinfer 条目
PRINFER_BACKEND、PRINFER_COMPILERTYPEPROBE_BACKEND、TYPEPROBE_COMPILER(未设置新名称时仍会读取旧名称)
错误名称 PrinferErrorTypeprobeError,现已导出;PrinferError 是同一类的弃用别名,因此使用任一名称进行 instanceof 检查都有效
Claude Code 插件 prinfer@prinfer/plugin uninstall prinfer@prinfer、/plugin marketplace remove prinfer,然后执行 安装 下的两个命令
MCP Registry io.github.clockblocker/prinfer、Smithery clockblocker/prinferio.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。

  1. 按常规发布 typeprobe:运行 bun run version,提交 chore: release typeprobe 4.0.0,然后运行 bun run release(npm),并打上标签 v4.0.0。使用 npx -y [email protected] --version 检查。
  2. 发布 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。
  3. 弃用所有 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"
    
  4. 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
    
  5. Smithery:运行 bun run mcpb,然后运行 npx -y [email protected] mcp publish ./typeprobe.mcpb -n clockblocker/typeprobe(需要 SMITHERY_API_KEY)。在 Smithery 的设置中,将旧的 clockblocker/prinfer 条目指向新条目,或取消发布该条目。

许可证

MIT

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。