示例
本指南展示 Nudo 类型推断的实用示例,按主题分组。每个示例包含带指令的输入代码和推断出的类型。
下方所有 输出块都是对上面代码真实运行 nudo infer 的结果。输出块只展示 case 头与 Observed: 行——它们是逐调用点的真实精度。完整输出里的 intension: / abs: 行是用 unknown 形参重估的泛化签名,对多分支函数只会显示回退路径的结果;分支级精度请以 case 头与 Observed: 为准。当调用点路径更精确时示例使用调用点(call@L…)形态,否则使用 @nudo:case 指令。
仓库内 CI 自验证的示例套件在
docs/examples/:其中每条命令与承诺的退出码都由pnpm run verify:examples校验,并有逐示例的输出钉对照文档声称的输出行。本指南按主题浏览同一引擎;仓库套件是真值门禁。
基础推断
1. 带字面量与符号 case 的基本函数
一个函数具有多个 case:具体值和符号类型值。Nudo 会合并结果。
/**
* @nudo:case "positive numbers" (5, 3)
* @nudo:case "negative result" (1, 10)
* @nudo:case "symbolic" (number(), number())
*/
function subtract(a, b) {
return a - b;
}
推断输出:
=== subtract ===
debug "positive numbers": (5, 3) => 2
debug "negative result": (1, 10) => -9
debug "symbolic": (number, number) => number
Observed: number
具体 case 保留其字面量结果(2、-9),符号 case (number(), number()) 产生 number。合并类型是所有 case 结果的并集,并按吸收律化简——字面量被基类型 number 吸收,得到 number。
2. 带类型收窄的对象操作
属性访问。Nudo 通过对象形状推断类型,字符串拼接保留字面量结构。
function greet(user) {
return user.name + " is " + user.age;
}
greet({ name: "Alice", age: 30 });
推断输出:
=== greet ===
call@L4: ({ name: "Alice", age: 30 }) => "Alice is 30"
Nudo 用具体形状求值该调用:user.name 与 user.age 解析为字面量值,+ 拼接产生精确结果 "Alice is 30"——而不是被拍平的 string。
目前参数解构不会拆开实参形状——同样的函数体与调用写成 function greet({ name, age }) 会返回 number | string(解构出的字段以 unknown 到达,+ 因而拓宽为其普通 JS 语义的结果),因此要获得形状级精度,属性访问是可靠写法。
spread 形状合并是配置对象的主力工具——右侧槽位覆盖同名左侧槽位,其余槽位取并集,每个调用点保留自己的字面量:
function mixin(base, ext) {
return { ...base, ...ext };
}
mixin({ host: "localhost", port: 8080 }, { port: 3000, debug: true });
mixin({ id: 1 }, { name: "ada" });
=== mixin ===
call@L4: ({ host: "localhost", port: 8080 }, { port: 3000, debug: true }) => { host: "localhost", port: 3000, debug: true }
call@L5: ({ id: 1 }, { name: "ada" }) => { id: 1, name: "ada" }
Observed: { host: "localhost", port: 3000, debug: true } | { id: 1, name: "ada" }
字面量 key 的索引投影会精确取出对应槽位——对扮演 "env" 角色的对象同样精确:
function pick(obj, key) {
return obj[key];
}
pick({ a: 1, b: "x" }, "a");
const env = { PATH: "/usr/bin", HOME: "/root" };
pick(env, "PATH");
=== pick ===
call@L4: ({ a: 1, b: "x" }, "a") => 1
call@L6: ({ PATH: "/usr/bin", HOME: "/root" }, "PATH") => "/usr/bin"
Observed: 1 | "/usr/bin"
符号 key(string())无法选定槽位,退化为 unknown——仓库示例(CI 钉住):docs/examples/algebra/e-index-proj.js。spread meet 钉在 docs/examples/algebra/d-mixin-meet.js;--dts 投影(单一拓宽签名、字面量并返回)由示例矩阵的 a-spread-optional.js --dts 行钉住——生成的 a-spread-optional.d.ts 即真值输出。
3. 使用 map 的数组处理
数组和高阶函数。Nudo 通过 map 和 filter 跟踪元素类型。
/**
* @nudo:case "concrete" ([1, 2, 3])
* @nudo:case "symbolic" (array(number()))
*/
function doubleAll(arr) {
return arr.map((x) => x * 2);
}
推断输出:
=== doubleAll ===
debug "concrete": ([1, 2, 3]) => [2, 4, 6]
debug "symbolic": (number[]) => number[]
Observed: [2, 4, 6] | number[]
Nudo 通过 map 跟踪元素类型。具体输入 [1, 2, 3] 被逐元素求值为 [2, 4, 6],符号输入 array(number()) 产生 number[]。仓库示例(CI 钉住):docs/examples/algebra/b-hof-map.js。
reduce 同样精确——字面量数组经累加器逐元素折叠,符号数组单次应用回调(init + element → number):
/**
* @nudo:case "literal" ([1, 2, 3, 4, 5])
* @nudo:case "symbolic" (array(number()))
*/
function sum(numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
=== sum ===
debug "literal": ([1, 2, 3, 4, 5]) => 15
debug "symbolic": (number[]) => number
Observed: number
数组方法支持并不均匀——依赖某个方法前先查这条边界。some / every 在调用点与 @nudo:case 两条路径上都折叠为 boolean。forEach 回调的副作用在两条路径上都写进内部 Abs(abs: 15 #exact),但 case 头(外延投影)不同:@nudo:case 指令路径报告终值 15,调用点路径报告循环前的 0:
function forEachSum(arr) {
let s = 0;
arr.forEach((x) => { s = s + x; });
return s;
}
forEachSum([1, 2, 3, 4, 5]); // → 0 —— 调用点路径的 case 头(abs: 15 #exact)
function someBig(arr) {
return arr.some((x) => x > 3);
}
someBig([1, 2, 3, 4, 5]); // → boolean
指令路径才是 case 头能反映 forEach 写回的路径——仓库示例钉住 @nudo:case "forEach" → 15 #exact。调用点若需要精确报告值,请改用 map / reduce(以及 filter → map → reduce 链,逐级保留字面量精度)。仓库示例(CI 钉住):docs/examples/algebra/c-reduce-sum.js、docs/examples/algebra/h-array-boundary.js。
异步调用与错误
4. 带 mock fetch 的异步函数
异步函数和外部 API。使用 @nudo:mock 将 fetch(或其他全局对象)替换为 body 为普通 JavaScript 的 mock,且必须写在单行内。
/**
* @nudo:mock fetch = (url) => ({ ok: true, json: () => ({ id: 1, name: "Alice" }) })
* @nudo:case "user" (1)
*/
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
推断输出:
=== fetchUser ===
debug "user": (1) => promise<{ id: 1, name: "Alice" }>
mock 就位后,Nudo 推断 fetchUser 返回 promise<{ id: 1, name: "Alice" }>,无需真实网络请求。内联 mock 有两条硬性规则:表达式必须单行(多行会被截断并报 nudo:mock-invalid);mock body 内不可用约束构建器——只能写普通 JavaScript 值和闭包。stub().resolves(...) helper 只在纯数据上等价:字面量槽位保留(stub().resolves({ ok: true, id: 1 }) → promise<{ ok: true, id: 1 }>),但 resolved 值里的闭包槽位不被桥接——json 到达时无 body(json: () => ?),于是 res.json() 求值为 unknown,本示例退化为 promise<unknown>。mock 结果要被调用时,用上面的箭头函数形态。仓库示例(CI 钉住):docs/examples/algebra/f-async-eff.js——其中 @nudo:mock 是必填而非可选:没有它,B 路径会执行真实 fetch 并以 ERR_INVALID_URL 崩溃。
5. 带 throws 追踪的错误处理
会抛出的函数。Nudo 同时追踪正常返回类型和抛出类型。
/**
* @nudo:case "valid" (10)
* @nudo:case "negative" (-1)
*/
function half(x) {
if (x < 0) {
throw new RangeError("negative input");
}
return x / 2;
}
推断输出:
=== half ===
debug "valid": (10) => 5
debug "negative": (-1) => never throws RangeError
Observed: 5
Nudo 建模控制流:valid case 返回 5,negative case 抛出 RangeError 且永不返回——其结果为 never,同时追踪抛出的值。合并后的值类型为 5。像这样静态可判定的 throw 不会产生额外诊断——never throws RangeError 就是全部信息。只有条件性 throw(抛出分支由 unknown 条件守卫,如示例 15)才会为对应 case 追加报告 nudo-may-throw。