reactjs - Typescript - kako kombinirati unije i presjeke tipova


Imam sljedeću komponentu:

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>

Ovo je malo pojednostavljeno kako bi se moglo postaviti ovo pitanje. Poanta je da pokušavam napraviti Discriminated Union zajedno s tipovima presjeka. Pokušavam postići željena svojstva na temelju vrijednosti oznake. Dakle, ako se koristi Tags.button, tada se koriste JSX-ove atribute za gumb (i href u gornjem primjeru treba stvoriti pogrešku jer nije dopušteno na button elementu) - međutim, druga složenost je da bih želio koristiti ili a ili b, ali se ne mogu koristiti zajedno - zbog toga su tu tipovi presjeka.

Što radim pogrešno ovdje i zašto tip radi očekivano samo kada dodam svojstvo a ili b?

Ažuriraj

Dodao sam igralište s primjerima kako bih pokazao kada bi trebalo doći do pogreške, a kada bi trebalo kompajlirati.

dječje igralište

Odgovor #1

U vašem primjeru postoje 2 problema koja se moraju riješiti i oba proizlaze iz istog "problema" (značajke).

U Typescriptu sljedeće ne funkcionira onako kako bismo ponekad željeli:

interface A {
  a?: string;
}

interface B {
  b?: string;
}

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

Ono što želite je eksplicitno isključiti B iz A, i A iz B - tako da ne mogu pojaviti zajedno.

Ovo pitanje raspravlja o "XOR"anju tipova i predlaže korištenje paketa ts-xor ili pisanje vlastitog. Evo primjera iz jednog odgovora tamo (isti kod se koristi u 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;

Sada, s ovim možemo napokon riješiti vaš problem:

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

Preciznije, vaši tipovi će biti:

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

Odgovor #2

Morate učiniti da a i b nisu opcionalni, već koristiti unijski tip (|) koji odgovara inkluzivnom ILI. On se presijeca s neuvjetnim svojstvima (never). Tako sada TypeScript može ispravno razlikovati između njih: Ažuriraj

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

Evo igrališta.

Povezani članci