条件道具的 TypeScript 实用程序类型(基于类型中其他属性的输入值) [英] TypeScript utility type for conditional props (based on entered value of other properties in the type)

查看:20
本文介绍了条件道具的 TypeScript 实用程序类型(基于类型中其他属性的输入值)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我经常需要定义一个类型对象,其中一个属性键仅在该类型的另一个/多个属性是特定值时才被接受.

I frequently need to define a type object where a property key is only accepted if another property/properties of the type are a certain value.

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

A simple example (in the context of React but should be applicable in any situation) is I need a type Button object which accepts the following properties:

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

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

Now, I actually don't want the type to accept hasFancyOutline if appearance is not outline and isDisabled is 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 的速记实用程序类型,它可以智能地为我执行此操作.像这样:

I want to write a shorthand utility type called ConditionalProps that intelligently does this for me. Something like this:

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

我正在考虑使用伪代码,它会像这样工作:

I'm thinking in pseudo-code, it'd work something like this:

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 重写得更好.

我发现自己编写了很多以给定值为条件的类型.

I find myself writing a lot of types that are conditional on values it is given.

例如,

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

说我想做:

  1. isReallyBig? 仅在 size = 'large'
  2. 时被接受
  3. hasFancyOutline? &outlineBackgroundColor 仅在 appearance = ‘outline’ & 时被接受isDisabled = false
  4. isLoading 只能在 isDisabled = true 时为 true.
  1. isReallyBig? is accepted only if size = 'large'
  2. hasFancyOutline? & outlineBackgroundColor is accepted only if appearance = ‘outline’ & isDisabled = false
  3. isLoading can be true only if isDisabled = true.

如果我想重新编写 ConditionalProps 来清晰地定义这种类型,我该怎么做?我在想实现应该是这样的:

If I wanted to re-write ConditionalProps to cleanly define this type, how would I do so? I was thinking implementation would be something like:

  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 }
      ]
    ]
  >

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

Is something like this achievable, or is there a better way of dealing with this scenario?

推荐答案

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

While implementing this, the problem I had was that it was not obvious why only appearance should have it's values removed from the common case. isDisabled is a union of true | false so, removing all values from the common case would result in false being removed from isDisabled on the default case. This is probably not the desired behavior.

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

If we add a property to spell out what the discriminant is, we can build the type you want

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 case定义的属性,我们需要获取公共属性(Omit)并与TOmit相交代码>.

RemoveCommonValues goes through the common properties, and if they are defined on TOmit removed the values that are defined there from the common values. To get the properties defined by the TOmit case, we need to get the common properties (Omit<T, keyof TOmit>) and intersect them with 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;
}

我们可以传入多种情况:

We can pass in multiple cases:

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;
}

如果我们想要更多的判别键,那么它是如何工作的还不清楚,因为它组合得不好.您可以传入多个键,但必须确保传入的情况涵盖所有可能的组合,因为所有值都将从结果中删除:

If we want to have more discriminant keys its not clear how that would work though as this does not compose well. You can pass in multiple keys, but you have to make sure the cases you pass in cover all possible combinations as any values will all be removed from the result:

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 按钮是可能的.

No minimal large button is possible.

这篇关于条件道具的 TypeScript 实用程序类型(基于类型中其他属性的输入值)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆