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 自動完成和更乾淨的編譯器錯誤。