跳到主要内容

运行时类型生成

Nudo 的类型推断不仅止于静态分析。你可以直接从推断的类型生成运行时验证器,在开发时推断和生产时验证之间建立无缝桥梁。

JS code → Nudo infers types → Generate validators → Runtime validation

这意味着你编写纯 JavaScript,让 Nudo 推断类型,然后生成完整的运行时类型检查——无需手写验证器,无需重复的类型定义。

所有生成结果默认打印到 stdout。可传 --output <dir> 写入文件。

nudo generate 命令​

nudo generate <file> [options]
选项描述
--format <format>输出格式:zod、guard、dts、all(默认:all)
--output <dir>把校验器文件写入该目录(<name>.nudo.zod.ts、<name>.nudo.guard.ts、<name>.d.ts)。省略则打印到 stdout。

运行 nudo generate 会读取源文件的推断类型,并以指定格式打印验证器。

基本用法​

# 打印所有格式(zod、guard、dts)
nudo generate src/api/users.js

# 仅打印 Zod schema
nudo generate src/api/users.js --format zod

# 自行把 stdout 捕获到文件
nudo generate src/api/users.js --format zod > users.schema.txt

示例源码​

本页所有示例都使用下面的文件。指令类型表达式使用约束构建器(number()、string()、shape({...})、array(...))或具体字面量——不是已移除的 T.* 文法。@nudo:case 见证仅用于调试。

// src/api/users.js

// @nudo:case "input" (shape({ name: string(), age: number() }))
function createUser(input) {
return { id: 123, name: input.name, age: input.age };
}

Zod Schema 生成​

使用 --format zod 时,Nudo 会为每个 case 的输入和输出类型打印 Zod schema 表达式。schema 以注释形式输出——把其中的表达式复制出来,组装成你自己的 schema 模块。

nudo generate src/api/users.js --format zod

输出(stdout):

// === createUser Zod Schemas ===
// debug "input":
// Input: { arg0: z.object({ name: z.string(), age: z.number() }) }
// Output: z.object({ id: z.literal(123), name: z.string(), age: z.number() })

注意 z.literal(123):源码中的字面量值(id: 123)会被推断为字面量类型,因此输出的 schema 会钉住精确值。

组装 Schema 模块​

把打印出的表达式粘贴到模块中并导出:

// src/api/users.schema.js -- 由上面的输出来组装
import { z } from "zod";

export const createUserInput = z.object({ name: z.string(), age: z.number() });
export const createUserOutput = z.object({ id: z.literal(123), name: z.string(), age: z.number() });

与框架集成​

React Hook Form ——将组装好的 schema 用作表单解析器:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createUserInput } from "./api/users.schema.js";

const { register, handleSubmit } = useForm({
resolver: zodResolver(createUserInput),
});

tRPC ——在过程中使用 schema 进行输入/输出验证:

import { createUserInput, createUserOutput } from "./api/users.schema.js";

const appRouter = router({
createUser: publicProcedure
.input(createUserInput)
.output(createUserOutput)
.mutation(({ input }) => createUser(input)),
});

Next.js API Routes ——验证请求体:

import { createUserInput } from "./api/users.schema.js";

export async function POST(request) {
const body = await request.json();
const parsed = createUserInput.safeParse(body);
if (!parsed.success) {
return Response.json({ errors: parsed.error.issues }, { status: 400 });
}
const user = createUser(parsed.data);
return Response.json(user);
}

原生 Guard 生成​

使用 --format guard 时,Nudo 会打印零依赖的运行时类型守卫函数。这些是纯 JavaScript 函数,没有外部导入,非常适合库、边缘函数或任何关注包体积的场景。

Guard 的命名为 is + 函数名 + case 名 + Output(每个 case 一个 guard,校验该 case 的输出类型):

nudo generate src/api/users.js --format guard

输出(stdout):

// === createUser Type Guards ===
export function iscreateUserInputOutput(data) {
return typeof data === "object" && data !== null && data.id === 123 && typeof data.name === "string" && typeof data.age === "number";
}

把打印出的函数保存到模块中(例如 src/api/users.guard.js)并导入使用。

性能优势​

Guard 函数执行一系列 typeof 检查,没有 schema 解释开销。在基准测试中,手写或生成的 guard 在验证密集型工作负载中始终比 schema 解释器(Zod、Yup、io-ts)快 2-10 倍。在高频验证大型负载时,这个差异会累积。

TypeScript 声明​

使用 --format dts 时,Nudo 为每个函数打印一条拓宽后的单一签名——与 nudo infer <file> --dts 输出一致。有三点需要了解:

  • 参数名来自源码(如 input);只有声明节点无法恢复名称时才回退为按位置的 arg0、arg1。
  • 参数位置(逆变位)会被拓宽:字面量参数坍缩为基类型("hello" → string、[1, 2, 3] → number[]),调用方可以传入任意兼容值。返回类型保留推断精度,包括嵌套字面量。
nudo generate src/api/users.js --format dts

输出(stdout):

// === createUser TypeScript Declarations ===
/**
* @param input - { name: string; age: number }
* @returns { id: 123; name: string; age: number }
*/
export declare function createUser(input: { name: string; age: number }): { id: 123; name: string; age: number };

存在多个 @nudo:case 指令时签名仍然是单一的——参数跨 case 取联合并拓宽,每个 case 的精确结果保留在 JSDoc 中:

// @nudo:case "string input" ("hello")
// @nudo:case "number input" (42)
function formatValue(value) {
return `${value}`;
}
nudo generate src/api/format.js --format dts
// === formatValue TypeScript Declarations ===
/**
* Case: string input ("hello") => "hello"
* Case: number input (42) => "42"
* @param value - string | number
* @returns string
*/
export declare function formatValue(value: string | number): string;

如果想把 .d.ts 文件直接写到源码旁边而不是打印,可使用 nudo infer <file> --dts。

JSON 输出​

用于程序化消费和 CI/CD 集成时,使用 nudo infer --json 获取机器可读的输出。

nudo infer src/api/users.js --json

输出结构:

{
"version": 1,
"file": "src/api/users.js",
"summary": {
"functions": 1,
"externalFunctions": 0,
"cases": 1,
"diagnostics": 0
},
"functions": [
{
"name": "createUser",
"loc": {
"start": {
"line": 4,
"column": 0
},
"end": {
"line": 6,
"column": 1
}
},
"entryOnly": false,
"cases": [
{
"name": "input",
"args": [
"{ name: string, age: number }"
],
"result": "{ id: 123, name: string, age: number }",
"throws": null,
"source": "directive",
"intension": {
"display": "createUser: (input: A1) => { id: 123, name: unknown, age: unknown }",
"abs": "{ id: 123, name: string, age: number } #exact",
"absMultiline": "createUser\n { id: 123, name: string, age: number }\n conf: exact",
"conf": "exact"
}
}
],
"combined": "{ id: 123, name: string, age: number }"
}
],
"diagnostics": []
}

functions 中的每个条目包含:

  • name 与 loc——函数名及其源码位置。
  • cases——每个 case 一条。args 列出参数类型,result 是返回类型,throws 是抛出类型或 null。source 对 @nudo:case 指令为 "directive",对由全程序调用点发现合成的 case 为 "callsite",对没有调用点的 entry@L 兜底 case 为 null。每个 case 还携带一个 intension 对象,内含无损 Abs 签名。
  • combined——所有 case 结果的并集,经吸收律化简。
  • entryOnly——当函数在整个程序中没有调用点时为 true。

CI/CD 集成​

在管道中使用 JSON 输出来强制类型契约。infer 接受文件路径,不接受目录:

# 如果报告了任何诊断则失败
nudo infer src/api/users.js --json | jq '.diagnostics | length == 0'

在构建中打印验证器,并把 stdout 捕获进项目:

{
"scripts": {
"generate": "nudo generate src/api/users.js --format zod > src/api/users.schema.txt",
"build": "npm run generate && tsc && vite build"
}
}

完整工作流​

以下是从源代码到运行时验证的端到端示例。

1. 编写带一条 Nudo 指令的纯 JavaScript:

// src/api/products.js

// @nudo:case "input" (shape({ name: string(), price: number(), tags: array(string()) }))
function createProduct(input) {
return {
id: 456,
name: input.name,
price: input.price,
tags: input.tags,
};
}

2. 打印所有验证器格式:

nudo generate src/api/products.js --format all

输出(stdout):

// === createProduct Zod Schemas ===
// debug "input":
// Input: { arg0: z.object({ name: z.string(), price: z.number(), tags: z.array(z.string()) }) }
// Output: z.object({ id: z.literal(456), name: z.string(), price: z.number(), tags: z.array(z.string()) })

// === createProduct Type Guards ===
export function iscreateProductInputOutput(data) {
return typeof data === "object" && data !== null && data.id === 456 && typeof data.name === "string" && typeof data.price === "number" && Array.isArray(data.tags) && data.tags.every((item) => typeof item === "string");
}

// === createProduct TypeScript Declarations ===
/**
* @param input - { name: string; price: number; tags: string[] }
* @returns { id: 456; name: string; price: number; tags: string[] }
*/
export declare function createProduct(input: { name: string; price: number; tags: string[] }): { id: 456; name: string; price: number; tags: string[] };

3. 把需要的部分粘贴进你的应用:

// src/api/products.guard.js -- 粘贴自上面的 stdout
export function iscreateProductInputOutput(data) {
return typeof data === "object" && data !== null && data.id === 456 && typeof data.name === "string" && typeof data.price === "number" && Array.isArray(data.tags) && data.tags.every((item) => typeof item === "string");
}
// src/api/products.schema.js -- 由上面的 Zod 行组装
import { z } from "zod";

export const createProductInput = z.object({ name: z.string(), price: z.number(), tags: z.array(z.string()) });
import { iscreateProductInputOutput } from "./api/products.guard.js";
import { createProductInput } from "./api/products.schema.js";

// 快速 guard 检查(零依赖)
if (!iscreateProductInputOutput(body)) {
throw new ValidationError("Invalid product data");
}

// 或使用 Zod 获取详细错误信息
const result = createProductInput.safeParse(body);
if (!result.success) {
return Response.json({ errors: result.error.issues }, { status: 400 });
}

4. 在消费端 TypeScript 代码中使用声明实现类型安全:

把声明行粘贴到源码旁的 .d.ts 中:

// src/api/products.d.ts -- 粘贴自上面的 stdout
/**
* @param input - { name: string; price: number; tags: string[] }
* @returns { id: 456; name: string; price: number; tags: string[] }
*/
export declare function createProduct(input: { name: string; price: number; tags: string[] }): { id: 456; name: string; price: number; tags: string[] };
// 消费端代码无需任何手动注解即可看到完整类型
import { createProduct } from "./api/products.js";

const product = createProduct({ name: "Widget", price: 9.99, tags: ["sale"] });
// ^? { id: 456; name: string; price: number; tags: string[] }

每个函数只需一行指令,这个工作流就能在 JavaScript/TypeScript 边界上提供完整的运行时安全和编辑器支持。