網站搜尋

映射類型 - 如何在 Typescript 中強制屬性之間的相互依賴


我正在嘗試在一般的 TypeScript 類型中強制屬性之間的相互依賴。上下文是一個可重用的 React 元件,它接收 props。例如,如果元件接收到按鈕的文字,那麼它也必須接收到該按鈕的處理函數;但如果元件沒有接收到文字,那麼處理函數的 prop 也應該為 undefined。

這是我的嘗試:

type InterConnectedProps<
  T extends string | undefined,
  PropertyName extends string | undefined,
> = T extends string
  ? ({
      [K in T]: string;
    } & { [K in PropertyName]: () => void })
  : {
      T?: undefined;
      PropertyName?: undefined;
    };

    //Alternatively:
    
    type InterConnectedProps2<
  T extends string | undefined,
  PropertyName extends string | undefined,
> = T extends string
  ? ({
      [K in T]: string;
      //The differnce lies here: "as string"
    } & { [K in PropertyName as string]: () => void })
  : {
      T?: undefined;
      PropertyName?: undefined;
    };



type Props = {
  title: string;
  subtitle?: string;
}& InterConnectedProps2<'button1', 'handlerBtn1'> & InterConnectedProps2<'button2', 'handlerBtn2'>;

// Props should be either:
//  {title: string; subtitle?:string; button1: string; handlerBtn1 : () => void}
// or {title: string; subtitle?:string; button1: undefined; handlerBtn1 : undefined}
//Anyway, there should be no handlerBtn1 if there is no button1

但是這不起作用,不僅是因為我沒有驗證 PropertyName 是字串(我該如何引入多重條件?)。即使我寫 { [K in PropertyName as string]:() => void },結果的類型似乎也不包含第二個參數(PropertyName):

type Button=InterConnectedProps<'button1', 'btn1Handler'> 只是 {button1: string}。

我哪裡做錯了,還是有其他的選擇?

謝謝!

答案#1

看起來你想要定義一個工具型別 AllOrNone<T>,其中 T 是一個具有所有必填屬性的物件型別,而其結果型別將接受一個 T 型別的物件,或一個不包含 T 的任何屬性的物件。

這裡有一種可能的定義方式:

type AllOrNone<T extends object> =
  T | { [P in keyof T]?: never }

這是一個聯合類型,用來表示「二擇一」的部分。一個聯合成員是 T,另一個是 { [P in keyof T]?: never },這在 TypeScript 中是我們能盡可能接近「一個不包含 T 任何屬性的物件」的方式。

讓我們更詳細地檢視 { [P in keyof T]?: never }。這是一個映射類型,其中 T 的每個屬性鍵都被設為可選,而值的類型是不可能的 never 類型。因此,如果 T 是 {a: 0, b: 1, c: 2},那麼 {[P in keyof T]?: never} 就是 {a?: never, b?: never, c?: never}。TypeScript 並沒有真正的方式來 禁止 一個屬性。但是,型別為 never 的可選屬性接近於禁止:你無法提供 never 類型的值,因此你唯一的“選擇”就是完全不提供這個屬性(或者根據你的編譯器設定將其設為 undefined)。


讓我們來測試一下:

type Z = AllOrNone<{ a: 0, b: 1, c: 2 }>;
/* type Z = { a: 0, b: 1, c: 2 } | 
     { a?: never, b?: never, c?: never } */

let z: Z;
z = {a: 0, b: 1, c: 2}; // okay
z = {a: 0, b: 1}; // error!
z = {a: 0}; // error!
z = {}; // okay

這有道理;Z 要麼是 {a: 0, b: 1, c: 2},要麼是一個沒有這些屬性的物件。


現在我們可以使用兩次 AllOrNone 來定義 Props:

type Props = { title: string; subtitle?: string; } &
  AllOrNone<{ button1: string, handlerBtn1(): void }> &
  AllOrNone<{ button2: string, handlerBtn2(): void }>;

let p: Props;
p = { title: "" };
p = { title: "", button1: "", handlerBtn1() { } };
p = { title: "", button2: "", handlerBtn2() { } };
p = { title: "", button1: "", handlerBtn2() { } }; // error

看起來不錯!

程式碼的 Playground 連結