Kako kombinirati unije u TypeScriptu?


Imam

type a = { a: "hi", b: number }
type b = { a: "bye", b: number }

Želim nekako napraviti :

type c = a + b // there's nothing called "+"

i da se c zaključi kao

type c = { a: "hi" | "bye", b: number };

Pokušao sam napraviti:

import { UnionToIntersection } from 'utility-types'

type a = { a: "hi", b: number }
type b = { a: "bye", b: number }
type c = UnionToIntersection<a | b> // never

Ali c se tumači kao never.


uredi*

Ako ti ne smeta, evo još jedan slučaj upotrebe

type o = {
  type: "bar";
  foo: string;
  bar: number;
} | {
  type: "baz";
  foo: string;
  baz: boolean;
}

Želim na neki način pretvoriti ovu vrstu u sljedeću vrstu:

type: "bar" | "baz"  ;
foo: string;
bar?: number;
baz?: boolean;

Odgovor #1

Ono što tražite vrlo je blisko operaciji identiteta; ako imate unijski tip poput A | B i pristupate njegovim svojstvima, svako od svojstava automatski će postati unija. Ako je to sve što ste željeli, možete jednostavno koristiti uniju takvu kakva jest, ili ako želite, napisati mapirani tip kako biste sveli uniju u jedan objektni tip.

Jedini problem ovdje je što želite da svojstva koja se pojavljuju samo u nekim članovima unije budu opcionalna u kombiniranom tipu, dok će unija obično jednostavno izostaviti takva svojstva. Dakle, prva stvar koju bismo trebali učiniti je pretvoriti svakog člana unije u novi tip s opcionalnim svojstvima tipa never za bilo koje svojstvo koje nije definirano u tom članu. To jest, želimo pretvoriti {a: 0, b: 1} | {b: 2, c: 3} u nešto poput {a: 0, b: 1, c?: never} | {a?: never, b: 2, c: 3}. To će se zatim kombinirati kako bi se formiralo {a?: 0, b: 1 | 2, c?: 3} kao što je željeno kada se spoje.

Spojiti to zajedno izgleda ovako:

type _Combine<T, K extends PropertyKey = T extends unknown ? keyof T : never> =
    T extends unknown ? T & Partial<Record<Exclude<K, keyof T>, never>> : never;

type Combine<T> = { [K in keyof _Combine<T>]: _Combine<T>[K] }

Ovdje je _Combine<T> pomoćni tip koji koristi distributivne uvjetne tipove za razdvajanje T u članove unije i izvođenje operacija nad njima. Prvi je zadani tip argumenta za K koji prikuplja sve ključeve članova T (T extends unknown ? keyof T : never), a drugi je mjesto gdje presijecamo svakog člana T s objektom koji ima opcionalne ključeve tipa never za svaki ključ u K koji nije dio tog člana T.

I onda je Combine<T> tip mapiran identitetom preko _Combine<T> koji kolapsira ružne stvari poput ({a: 0, b: 1) & Partial<Record<"c", never>>) | ({b: 2, c: 3} & Partial<Record<"a", never>>) u željeni {a?: 0, b: 1 | 2, c?: 3}.


Isprobajmo to na tvom primjeru:

type O = {
    type: "bar";
    foo: string;
    bar: number;
} | {
    type: "baz";
    foo: string;
    baz: boolean;
}

type Z = Combine<O>;
/* type Z = {
    type: "bar" | "baz";
    foo: string;
    bar?: number | undefined;
    baz?: boolean | undefined;
} */

Izgleda dobro. Svojstva type i foo su obavezna, budući da postoje u svim članovima O, dok su svojstva bar i baz opcionalna, budući da nedostaju u barem jednom članu O.

Igralište poveznica na kod

Odgovor #2

Možeš samo koristiti

type c = a | b;

Budući da je b zajedničko svojstvo, ono će se spojiti bez promjena, a a će se spojiti kao "hi" | "bye".

Ovo bi neki IDE-ovi mogli prikazati kao { a: "hi", b: number } | { a: "bye", b: number } umjesto { a: "hi" | "bye", b: number }, ali je funkcionalno identično.

Pogledajte ovaj radni primjer: TS Playground

Povezani članci