Featured image of post 使用 TypeScript 的 NoInfer 实用程序进行严格类型保护Featured image of post 使用 TypeScript 的 NoInfer 实用程序进行严格类型保护

使用 TypeScript 的 NoInfer 实用程序进行严格类型保护

使用 NoInfer 实用程序类型可以防止复杂类型推断期间出现意外的扩展。

日益扩大的问题

当 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'

如果没有 NoInferhandler 参数将影响 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 自动完成和更干净的编译器错误。