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

Ste*_*Koo 7 typescript reactjs typescript2.0

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

\n\n

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

\n\n
type Button = {\n  size: 'small' | 'large';\n  appearance: 'solid' | 'outline' | 'minimal';\n  isDisabled?: boolean;\n  hasFancyOutline?: boolean;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,我实际上不希望类型接受hasFancyOutlineif appearanceis notoutlineisDisabledis false

\n\n

正确的方法是:

\n\n
type SharedButtonProps = {\n  size: 'small' | 'large';\n}\n\ntype NonOutlineButtonProps = SharedButtonProps & {\n  appearance: solid' | 'minimal';\n  isDisabled?: boolean;\n}\n\ntype OutlineButtonProps = SharedButtonProps & {\n  appearance: 'outline';\n  isDisabled: false;\n  hasFancyOutline?: boolean;\n}\n\ntype Button = NonOutlineButtonProps | OutlineButtonProps\n
Run Code Online (Sandbox Code Playgroud)\n\n

我想编写一个名为的速记实用程序类型ConditionalProps,它可以智能地为我完成此操作。像这样的东西:

\n\n
type Button = ConditionalProps<\n  {\n    size: 'small' | 'large';\n    appearance: 'solid' | 'outline' | 'minimal';\n    isDisabled?: boolean;\n  },\n  {\n    appearance: 'outline';\n    isDisabled: false;\n    hasFancyOutline?: boolean;\n  }\n>\n
Run Code Online (Sandbox Code Playgroud)\n\n

我在想伪代码,它的工作原理如下:

\n\n
type ConditionalProps<BaseProps, ConditionalProps> = {\n  // 1. Find keys with the same name in BaseProps & ConditionalProps. Optional and non-optional types such as `isDisabled?` and `isDisabled` need to be matched.\n\n  type MatchingProps = Match<BaseProps, ConditionalProps> // { appearance: 'solid' | 'outline' | 'minimal', isDisabled?: boolean }\n\n  type SharedProps = Omit<BaseProps, MatchingProps> // { size: 'small' | 'large' }\n\n  // 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'\n\n  type FailConditionProps = RemainingValues<MatchingProps, ConditionalProps> // { appearance: 'solid' | 'minimal'; isDisabled?: boolean; }\n\n  // 3. Assemble\n\n  type FailConditionPlusSharedProps = SharedProps & FailConditionProps\n\n  type PassConditionPlusSharedProps = SharedProps & ConditionalProps\n\n  return FailConditionPlusSharedProps | PassConditionPlusSharedProps\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

编辑

\n\n

下面提香的回答就是这个问题的确切解决方案。但我想知道是否有办法重写得ConditionalProps更好。

\n\n

我发现自己编写了很多类型,这些类型都以给定的值为条件。

\n\n

例如,

\n\n
  type Button = {\n    size: 'small' | 'large';\n    isReallyBig?: boolean;\n    appearance: 'solid' | 'outline' | 'minimal';\n    hasFancyOutline?: boolean;\n    outlineBackgroundColor: string;\n    isDisabled?: boolean;\n    isLoading?: boolean;\n  }\n
Run Code Online (Sandbox Code Playgroud)\n\n

说我想做:

\n\n
    \n
  1. isReallyBig?仅当以下情况时才被接受size = 'large'
  2. \n
  3. hasFancyOutline?仅当&outlineBackgroundColor时才接受appearance = \xe2\x80\x98outline\xe2\x80\x99&isDisabled = false
  4. \n
  5. isLoading只能是true如果isDisabled = true.
  6. \n
\n\n

如果我想重写ConditionalProps以清晰地定义这种类型,我该怎么做?我想实施会是这样的:

\n\n
  type Button = ConditionalProps<\n    {\n      size: 'small' | 'large';\n      appearance: 'solid' | 'outline' | 'minimal';\n      outlineBackgroundColor: string;\n      isDisabled?: boolean;\n    },\n    [\n      [\n        { size: 'large' },\n        { isReallyBig?: boolean }\n      ], [\n        { appearance: 'outline', isDisabled: false },\n        { hasFancyOutline?: boolean }\n      ], [\n        { isDisabled: true },\n        { isLoading?: boolean }\n      ]\n    ]\n  >\n
Run Code Online (Sandbox Code Playgroud)\n\n

这样的事情可以实现吗,或者有更好的方法来处理这种情况吗?

\n

Tit*_*mir 5

在实现这一点时,我遇到的问题是,为什么只appearance应该从常见情况中删除它的值并不明显。isDisabled是 so 的并集true | 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>>>
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

我们可以在多种情况下传递:

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;
}
Run Code Online (Sandbox Code Playgroud)

如果我们想要有更多的判别键,目前还不清楚这将如何工作,因为这并不能很好地组合。您可以传入多个键,但必须确保传入的情况涵盖所有可能的组合,因为任何值都将从结果中删除:

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;
}
Run Code Online (Sandbox Code Playgroud)

没有minimal large按钮是不可能的。