用于条件道具的TypeScript实用程序类型(基于该类型中其他属性的输入值)

时间:2019-06-04 05:21:37

标签: reactjs typescript typescript2.0

我经常需要定义一个类型对象,其中仅当该类型的另一个属性是某个特定值时才接受属性键。

一个简单的示例(在React的上下文中,但应该在任何情况下都适用)是我需要一个类型Button对象,该对象接受以下属性:

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

现在,如果hasFancyOutline不是appearance并且outlineisDisabled,我实际上不希望类型接受false

正确的方法是:

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
}

编辑

下面的提香答案是确切的解决方案。但是我想知道是否有一种方法可以重写ConditionalProps从而变得更好。

我发现自己写了很多类型,这些类型取决于给出的值。

例如,

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

说我想做:

    仅在isReallyBig? 时接受
  1. size = 'large'
  2. hasFancyOutline?outlineBackgroundColor仅在appearance = ‘outline’isDisabled = false
  3. 时被接受
  4. isLoading仅在true时可以是isDisabled = 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 个答案:

答案 0 :(得分:1)

在执行此操作时,我遇到的问题是,不清楚为什么只有appearance的值才应从普通情况中删除。 isDisabledtrue | false的并集,因此,从普通情况中删除所有值将导致在默认情况下从false中删除isDisabled。这可能不是所需的行为。

如果我们添加一个属性来说明区别是什么,我们可以构建您想要的类型

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按钮。