在具有默认值的反应道具中使用区分联合

Using discriminating unions in react props with a default

我有一个具有 3 个变体的 React 组件。我的道具是这样输入的:

enum VariantType {
  VARIANT_1 = "variant_1",
  VARIANT_2 = "variant_2",
  VARIANT_3 = "variant_3",
}

type BaseProps = {
  a: string;
}

type Variant1Props = BaseProps & {
  variant: VariantType.VARIANT_1;
  b: never;
}

type Variant2Props = BaseProps & {
  variant: VariantType.VARIANT_2;
  b: number;
}

type Variant3Props = BaseProps & {
  variant: VariantType.VARIANT_3;
  b: boolean;
}

export const FancyComponent: FunctionComponent<Variant1Props | Variant2Props | Variant3Props> = (props) => {
  const someNumber = props.variant === VariantType.VARIANT_2 ? props.b : 123; 
   ...
}

在someNumber的赋值中TS知道是定义了b因为我检查了变体。当使用组件时,当 b 存在时 TS 抱怨 VARIANT_1 并且当 b 不是 present/not 数字时它抱怨 VARIANT_2 (与 VARIANT_3).

我现在正在寻找的是一种在使用组件时使 variant 选项可选的方法,它应该默认为 VARIANT_1,您可以将其设置为 VARIANT_2VARIANT_3 而不会失去我现在拥有的任何类型安全性。

我尝试了泛型等等,但无法正常工作。

我正在使用 Typescript 4.5。

一旦你破坏了你的 props 就太晚了,因为它们不再相互关联。相反,您需要区分对象本身,然后进行解构。您可以通过以下几种方式做到这一点:

TS Playground

type AllVariants = Variant1Props | Variant2Props | Variant3Props;

// Example 1
export const FancyComponent1 = (props: AllVariants): ReactElement => {
  const someNumber = props.variant === VariantType.VARIANT_2 ? props.b : 123; // number
  return <div></div>;
}

// Example 2: Using conditional
export const FancyComponent2 = (props: AllVariants): ReactElement => {
  if (props.variant === VariantType.VARIANT_2) {
    const {
      a, // string
      b, // number
      variant, // VariantType.VARIANT_2
    } = props;
  }
  return <div></div>;
}


// Example 3: Extracting conditional in #2 to predicate:
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

function isVariant2 (props: AllVariants): props is Variant2Props {
  return props.variant === VariantType.VARIANT_2;
}

export const FancyComponent3 = (props: AllVariants): ReactElement => {
  if (isVariant2(props)) {
    const {
      a, // string
      b, // number
      variant, // VariantType.VARIANT_2
    } = props;
  }
  return <div></div>;
}

只需将 Variant1Props 中的 variant 属性 设为可选,一切正常:

enum VariantType {
  VARIANT_1 = "variant_1",
  VARIANT_2 = "variant_2",
  VARIANT_3 = "variant_3",
}

type BaseProps = {
  a: string;
}

type Variant1Props = BaseProps & {
  variant?: VariantType.VARIANT_1;
  b: never;
}

type Variant2Props = BaseProps & {
  variant: VariantType.VARIANT_2;
  b: number;
}

type Variant3Props = BaseProps & {
  variant: VariantType.VARIANT_3;
  b: boolean;
}

// to check that props.b actually has type never
declare function expectTheUnexpected(wtf: never): void;

export const FancyComponent = (props: Variant1Props | Variant2Props | Variant3Props) => {
  const someNumber = props.variant === VariantType.VARIANT_2 ? props.b : 123;
  if (!props.variant) expectTheUnexpected(props.b);
  // ...
}

in the Playground.

但实际上,您不希望 b: never——这是告诉 TS 期望(并要求)实际的 属性 b,但 属性 是 never。这意味着 { a: 'foo' } 不是有效的 Variant1Props,因此 <FancyComponent a="foo" /> 将不起作用。相反,有效的 Variant1Props 需要类似于 { a: 'foo', b: someVariableWithTypeNever },¹,这是一团糟。真的,never 很少是您实际期望使用的类型,它应该表示出现了问题,而我们现在处于类型系统认为不可能发生的情况。

你真正想要的Variant1Props定义是

type Variant1Props = BaseProps & {
  variant?: VariantType.VARIANT_1;
  b?: undefined;
}

现在 b 可能实际存在也可能不存在,但如果您包括它,它必须只是 undefined(这意味着它也可能不存在)。所以 { a: 'foo' } 有效,但 { a: 'foo', b: 42 } 无效——42 不是 undefined。要允许输入 b: 42,我们需要输入 variant: VariantType.VARIANT_2.

  1. 即该组件是用 <FancyComponent a="foo" b={someVariableWithTypeNever} /> 生成的——我将停止将 React 引入其中,它根本不会影响问题。