【问题标题】:TypeScript utility type for conditional props (based on entered value of other properties in the type)用于条件道具的 TypeScript 实用程序类型(基于类型中其他属性的输入值)
【发布时间】:2019-06-04 05:21:37
【问题描述】:

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

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

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

现在,如果appearance 不是outline 并且isDisabledfalse,我实际上不希望该类型接受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
}

编辑

以下提香的回答是对此的确切解决方案。但我想知道是否有办法重写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. 仅当 appearance = ‘outline’isDisabled = false 时才接受 hasFancyOutline?outlineBackgroundColor
  3. 仅当 isDisabled = true 时,isLoading 才能为 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 }
      ]
    ]
  >

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

【问题讨论】:

    标签: reactjs typescript typescript2.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&lt;T, keyof TOmit&gt;)并将它们与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 按钮是可能的。

    【讨论】:

    • 嗨,提香,太棒了!非常感谢您提供全面的答案。如果我想让ConditionalProps 更加灵活和可重复使用,你有什么建议?理想情况下,我希望使它成为一种基于各种值添加多个条件道具的简单方法。所以建立在上面的例子上:``` type Button = { size: 'small' | '大的';外观:'实心' | '大纲' | '最小'; isDisabled?:布尔值; hasFancyOutline?: 布尔值; isAllCaps?:布尔值;加载?:布尔值;加载文本?:字符串; } ```
    • @StephenKoo 欢迎您。如果您发现任何我可以提供帮助的问题,请告诉我。
    • 对不起,我把那个回复搞砸了——我可以PM你问更多关于你的答案吗?我正在努力提高高级 TS 类型。
    • @StephenKoo 你可以在 gitter 上私信我,我更喜欢 IM :) 虽然不知道如何让它更通用
    猜你喜欢
    • 1970-01-01
    • 2021-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多