網站搜尋

reactjs - TypeScript 用於條件屬性的工具類型(基於類型中其他屬性輸入的值)


我經常需要定義一個類型物件,其中一個屬性鍵僅在類型的另一個屬性/屬性為某個值時才被接受。

一個簡單的例子(在 React 的背景下,但應該適用於任何情況)是我需要一個 Button 類型的物件,它接受以下屬性:

type Button = {
  size: 'small' | 'large';
  appearance: 'solid' | 'outline' | 'minimal';
  isDisabled?: boolean;
  hasFancyOutline?: boolean;
}

現在,實際上我不希望該類型在 appearance 不是 outline 且 isDisabled 為 false 時接受 hasFancyOutline。

正確的做法是這樣:

type SharedButtonProps = {
  size: 'small' | 'large';
}

type NonOutlineButtonProps = SharedButtonProps & {
  appearance: solid' | 'minimal';
  isDisabled?: boolean;
}

type OutlineButtonProps = SharedButtonProps & {
  appearance: 'outline';
  isDisabled: false;
  hasFancyOutline?: boolean;
}

type Button = NonOutlineButtonProps | OutlineButtonProps

我想寫一個名為 ConditionalProps 的速記工具類型,它可以智能地為我做到這一點。類似這樣的東西:

type Button = ConditionalProps<
  {
    size: 'small' | 'large';
    appearance: 'solid' | 'outline' | 'minimal';
    isDisabled?: boolean;
  },
  {
    appearance: 'outline';
    isDisabled: false;
    hasFancyOutline?: boolean;
  }
>

我正在用偽代碼思考,它大概會這樣運作:

type ConditionalProps<BaseProps, ConditionalProps> = {
  // 1. Find keys with the same name in BaseProps & ConditionalProps. Optional and non-optional types such as `isDisabled?` and `isDisabled` need to be matched.

  type MatchingProps = Match<BaseProps, ConditionalProps> // { appearance: 'solid' | 'outline' | 'minimal', isDisabled?: boolean }

  type SharedProps = Omit<BaseProps, MatchingProps> // { size: 'small' | 'large' }

  // 2. Find what's the values of the props if they don't match the condition, e.g. 'appearance' would be either 'solid' or 'minimal'

  type FailConditionProps = RemainingValues<MatchingProps, ConditionalProps> // { appearance: 'solid' | 'minimal'; isDisabled?: boolean; }

  // 3. Assemble

  type FailConditionPlusSharedProps = SharedProps & FailConditionProps

  type PassConditionPlusSharedProps = SharedProps & ConditionalProps

  return FailConditionPlusSharedProps | PassConditionPlusSharedProps
}

編輯

下面 Titian 的回答是這個問題的確切解決方案。但我在想是否有辦法重寫 ConditionalProps 使其更好。

我發現自己寫了很多依據所給值而有條件的類型。

所以例如,

  type Button = {
    size: 'small' | 'large';
    isReallyBig?: boolean;
    appearance: 'solid' | 'outline' | 'minimal';
    hasFancyOutline?: boolean;
    outlineBackgroundColor: string;
    isDisabled?: boolean;
    isLoading?: boolean;
  }

說我想做:

  1. 只有當 size='large' 時,才接受 isReallyBig?
  2. hasFancyOutline? 與 outlineBackgroundColor 只有在 appearance=‘outline’ 且 isDisabled=false 時才被接受
  3. isLoading 只有在 isDisabled=true 時才能為 true。

如果我想重寫 ConditionalProps 以清楚地定義這個類型,我該怎麼做呢?我在想實作可能會像是這樣:

  type Button = ConditionalProps<
    {
      size: 'small' | 'large';
      appearance: 'solid' | 'outline' | 'minimal';
      outlineBackgroundColor: string;
      isDisabled?: boolean;
    },
    [
      [
        { size: 'large' },
        { isReallyBig?: boolean }
      ], [
        { appearance: 'outline', isDisabled: false },
        { hasFancyOutline?: boolean }
      ], [
        { isDisabled: true },
        { isLoading?: boolean }
      ]
    ]
  >

像這樣的事情可以實現,還是有更好的方法來處理這種情況?

答案#1

在實現這個的過程中,我遇到的問題是,不明顯為什麼只有 appearance 的值應該從常規情況中移除。isDisabled 是 true | false 的聯合類型,因此,從常規情況中移除所有值會導致在默認情況下 isDisabled 的 false 被移除。這可能不是期望的行為。

如果我們添加一個屬性來說明判別式是什麼,我們就可以建立你想要的類型

type Button = ConditionalProps<
  {
    size: 'small' | 'large';
    appearance: 'solid' | 'outline' | 'minimal';
    isDisabled?: boolean;
  }, 'appearance',
  {
    appearance: 'outline';
    isDisabled: false;
    hasFancyOutline?: boolean;
  }
>


type RemoveCommonValues<T, TOmit> = {
  [P in keyof T]: TOmit extends Record<P, infer U> ? Exclude<T[P], U> : T[P]
}

type Omit<T, K extends PropertyKey> = Pick<T, Exclude<keyof T, K>> // not needed in 3.5
type Id<T> = {} & { [P in keyof T]: T[P] } // flatens out the types to make them more readable can be removed
type ConditionalProps<T, TKey extends keyof TCase, TCase extends Partial<T>> =
  Id<Omit<T, keyof TCase> & TCase>
  | Id<RemoveCommonValues<T, Pick<TCase, TKey>>>

RemoveCommonValues 會遍歷共同屬性,如果這些屬性在 TOmit 上被定義,則會從共同值中移除那裡定義的值。要獲取 TOmit 情況下定義的屬性,我們需要獲取共同屬性 (Omit<T, keyof TOmit>) 並將它們與 TOmit 交集。

測試它:

type Button = ConditionalProps<
  {
    size: 'small' | 'large';
    appearance: 'solid' | 'outline' | 'minimal';
    isDisabled?: boolean;
  }, 'appearance',
  {
    appearance: 'outline';
    isDisabled: false;
    hasFancyOutline?: boolean;
  }
>
// same as 
type Button = {
    size: "small" | "large";
    appearance: "outline";
    isDisabled: false;
    hasFancyOutline?: boolean | undefined;
} | {
    size: "small" | "large";
    appearance: "solid" | "minimal";
    isDisabled?: boolean | undefined;
}

我們可以傳入多個案例:

type Button = ConditionalProps<
{
  size: 'small' | 'large';
  appearance: 'solid' | 'outline' | 'minimal';
  isDisabled?: boolean;
}, 'appearance' ,{
  appearance: 'outline';
  isDisabled: false;
  hasFancyOutline?: boolean;
} | {
  appearance: 'minimal';
  isDisabled: false;
  useReadableFont?: boolean;
}
>
// same as
type Button = {
    size: "small" | "large";
    appearance: "outline";
    isDisabled: false;
    hasFancyOutline?: boolean | undefined;
} | {
    size: "small" | "large";
    appearance: "minimal";
    isDisabled: false;
    useReadableFont?: boolean | undefined;
} | {
    size: "small" | "large";
    appearance: "solid";
    isDisabled?: boolean | undefined;
}

如果我們想要有更多判別鍵,但這是如何運作的就不太清楚,因為這不太容易組合。你可以傳入多個鍵,但你必須確保你傳入的案例涵蓋所有可能的組合,因為任何值都會從結果中被移除:

type Button = ConditionalProps<
{
  size: 'small' | 'large';
  appearance: 'solid' | 'outline' | 'minimal';
  isDisabled?: boolean;
}, 'appearance' | 'size' ,{
  appearance: 'outline';
  size: 'small'
  isDisabled: false;
  hasFancyOutline?: boolean;
} | {
  appearance: 'minimal';
  size: 'small'
  isDisabled: false;
  hasFancyOutline?: boolean;
}
>
// same as
type Button = {
    appearance: "outline";
    size: "small";
    isDisabled: false;
    hasFancyOutline?: boolean | undefined;
} | {
    appearance: "minimal";
    size: "small";
    isDisabled: false;
    hasFancyOutline?: boolean | undefined;
} | {
    size: "large";
    appearance: "solid";
    isDisabled?: boolean | undefined;
}

不可能有 minimal large 按鈕。