打字稿-如何结合并集和交叉点类型

时间:2020-05-24 09:26:50

标签: reactjs typescript union-types

我有以下内容:

export enum Tags {
  button = 'button',
  a = 'a',
  input = 'input',
}

type ButtonProps = {
  tag: Tags.button;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
  JSX.IntrinsicElements['button'];

type AnchorProps = {
  tag: Tags.a;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
  JSX.IntrinsicElements['a'];

type InputProps = {
  tag: Tags.input;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
  JSX.IntrinsicElements['input'];

type Props = ButtonProps | AnchorProps | InputProps;

const Button: React.FC<Props> = ({ children, tag }) => {
  if (tag === Tags.button) {
    return <button>{children}</button>;
  }
  if (tag === Tags.a) {
    return <a href="#">{children}</a>;
  }
  if (tag === Tags.input) {
    return <input type="button" />;
  }
  return null;
};

// In this instance the `href` should create a TS error but doesn't...
<Button tag={Tags.button} href="#">Click me</Button>

// ... however this does
<Button tag={Tags.button} href="#" a="foo">Click me</Button>

已将其剥离了一点,以便能够提出此问题。关键是我正在尝试使用交叉路口类型的区分联盟。我正在尝试根据标签值实现所需的道具。因此,如果使用Tags.button,那么将使用JSX的按钮属性(并且在上面的示例中,href会产生一个错误,因为在button元素上不允许这样做)–但是,其他复杂度是希望使用ab,但不能一起使用-因此相交类型。

我在这里做错什么,为什么在添加ab属性时该类型只能按预期工作?

更新

我添加了一个带有示例的游乐场,以显示何时应该出错以及何时应该编译。

playground

3 个答案:

答案 0 :(得分:4)

在您的示例中,有两个问题必须解决,并且都源于相同的“问题”(功能)。

在打字稿中,以下内容无法正常运行:

interface A {
  a?: string;
}

interface B {
  b?: string;
}

const x: A|B = {a: 'a', b: 'b'}; //works

您想要的是从A中显式排除B,从B中显式排除A-这样它们就不会一起出现。

This question讨论类型的“异或”,并建议使用软件包ts-xor或编写自己的软件包。这是答案的示例(ts-xor中使用了相同的代码):

type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

现在,有了这个,我们终于可以解决您的问题:

interface A {
  a?: string;
}

interface B {
  b?: string;
}

interface C {
  c?: string;
}

type CombinationProps = XOR<XOR<A, B>, C>;

let c: CombinationProps;
c = {}
c = {a: 'a'}
c = {b: 'b'}
c = {c: 'c'}
c = {a: 'a', b: 'b'} // error
c = {b: 'b', c: 'c'} // error
c = {a: 'a', c: 'c'} // error
c = {a: 'a', b: 'b', c: 'c'} // error

更具体地说,您的类型将是:

interface A {a?: string;}
interface B {b?: string;}

type CombinationProps = XOR<A, B>;

type ButtonProps = {tag: Tags.button} & JSX.IntrinsicElements['button'];
type AnchorProps = {tag: Tags.a} & JSX.IntrinsicElements['a'];
type InputProps = {tag: Tags.input} & JSX.IntrinsicElements['input'];

type Props = CombinationProps & XOR<XOR<ButtonProps,AnchorProps>, InputProps>;

答案 1 :(得分:1)

您必须将a和b设置为非可选,而是使用与包含性OR对应的并集类型(|)。它与非条件属性相交(从不相交)。因此,现在ts可以正确区分它们: 更新

type ButtonProps = {
  tag: Tags.button;
  a?: string;
  b?: string
} & 
  JSX.IntrinsicElements['button'];

这里是playground

答案 2 :(得分:0)

通常,我使用以下技术来创建通用功能组件,但它也适用于您的情况。技巧是将组件声明为function而不是const,以使其具有通用性。这样可以使道具变得通用,从而使您可以做到:

export enum Tags {
    button = 'button',
    a = 'a',
    input = 'input',
  }

  type Props<T extends Tags = Tags> = JSX.IntrinsicElements[T] & {
    tag: T;
  } & ({ a?: string; b?: never } | { a?: never; b?: string });

  function Button<T extends Tags>({ children, tag }: Props<T>) {
    if (tag === Tags.button) {
      return <button>{children}</button>;
    }
    if (tag === Tags.a) {
      return <a href="#">{children}</a>;
    }
    if (tag === Tags.input) {
      return <input type="button" />;
    }
    return null;
  }

  // These should error due to href not being allowed on a button element
  const a = <Button tag={Tags.button} href="#" a="foo">Click me</Button>
  const b = <Button tag={Tags.button} href="#">Click me</Button>

  // These should work
  const c = <Button tag={Tags.button} a="foo">Click me</Button>
  const d = <Button tag={Tags.button} b="foo">Click me</Button>
  const e = <Button tag={Tags.button}>Click me</Button>

  // This should error as `a` and `b` can't be used together
  const f = <Button tag={Tags.button} a="#" b='a'>Click me</Button>

唯一的缺点是您不能直接键入Button组件,不能说function Button<T>React.FC<Props<T>>,只能键入其props和return类型。

检查playground here(我将您的示例留在底部)

编辑,我用您问题中的示例更新了代码,并修复了游乐场链接

相关问题