日益扩大的问题
当 TypeScript 从多个调用点参数推断类型时,它通常会加宽推断的类型以适应所有候选类型。这通常很有用,但在某些通用场景中,它会生成过于松散的类型,从而让无效状态溜走。
考虑一个接受默认值和特定键的函数:
function createState<T extends string>(key: T, defaultValue: T) {
return { key, defaultValue };
}
const state = createState("color", "blue");
// T inferred as "color" | "blue" — not what we want
TypeScript 将 T 推断为 "color" | "blue",而不是限制 defaultValue 来匹配 key。结果类型失去了两个参数的精度。
输入 NoInfer
TypeScript 5.4 中引入的 NoInfer<T> 告诉编译器:不要使用此位置进行类型推断。该类型仍会根据 T 检查,但不会为 T 的解析提供候选项。
function createState<T extends string>(
key: T,
defaultValue: NoInfer<T>
) {
return { key, defaultValue };
}
const state = createState("color", "blue");
// T inferred as "color"
// "blue" is checked against "color" — OK
如果有人传递了不匹配的默认值,他们会得到一个明显的错误:
createState("color", 42);
// Error: Type 'number' is not assignable to type '"color"'
NoInfer 的工作原理
NoInfer<T> 在 lib.es5.d.ts 中定义为:
type NoInfer<T> = [T][T extends any ? 0 : never];
它使用条件类型来阻止推理。编译器在分配条件中看到 [T] 并将该位置视为不可推断。出于检查目的,实际类型仍解析为 T。
用例 1 — 函数参数
常见的模式是具有类型化有效负载的事件发射器:
function onEvent<E extends string, P>(
event: E,
handler: (payload: NoInfer<P>) => void
): void;
onEvent("click", (p: MouseEvent) => {}); // P = MouseEvent
onEvent("click", (p: number) => {});
// Error: 'number' is not assignable to 'MouseEvent'
如果没有 NoInfer,handler 参数将影响 P 的推断并可能扩大其范围。
用例 2 — 元组推理
当推断元组类型时,额外的候选项会导致扩展:
function pair<T extends readonly any[]>(
first: [...T],
second: NoInfer<[...T]>
): T;
const p = pair([1, "a"], [2, "b"]);
// T inferred as [number, string] — correct
// Without NoInfer: [number | 2, string | "b"] — too wide
用例 3 — 通用约束
防止从约束类型进行推断:
function lookup<T, K extends keyof T>(
obj: T,
key: K,
fallback: NoInfer<T[K]>
): T[K];
const val = lookup({ a: 1, b: "hello" }, "a", 0);
// val: number — correct
lookup({ a: 1 }, "a", "wrong");
// Error: 'string' is not assignable to 'number'
与替代方案的比较
| 方法 | 行为 |
|---|---|
没有 NoInfer | 类型扩大到所有候选人的联合 |
| 显式类型参数 | 调用者必须手动指定类型 |
NoInfer<T> | 防止从选定位置进行推断 |
| 单独的类型参数 | 增加了不必要的复杂性 |
何时使用 NoInfer
- 两个以上的参数共享一个类型参数,其中一个应该是推理源,另一个应该是检查目标。
- 带有推理的返回类型 — 防止调用站点参数扩大返回类型。
- 构建器模式 — 链式方法不应污染彼此的推断类型。
class Builder<T extends Record<string, unknown>> {
set<K extends string, V>(
key: K,
value: NoInfer<V>
): Builder<T & Record<K, V>> {
return this;
}
}
概括
NoInfer<T> 是一个用于加强通用推理的集中工具。它通过将特定参数位置标记为无推理来防止不必要的扩展,同时仍然强制类型兼容性。当一个参数应该锚定类型而其他参数应该符合它而不是扩展它时,请使用它。结果是更精确的类型、更好的 IDE 自动完成和更干净的编译器错误。

