typescript - Ako môžem napísať všeobecný TypeGuard pre GraphQL uniony?
Chcel by som napísať univerzálny Type Guard pre GraphQL unie.
Povedzme, že máme dva typy A a B, a C je ich zjednotený typ. A má vlastnosť a, ale B ju nemá. Myslím, že by mohlo byť užitočné, keby sme mohli mať typový stráž ako tento:
xs.filter(isTypename("a")).map(x => x.a)Snažil som sa chvíľu, ale stále som to nedokončil.
Je to možné dosiahnuť?
https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgILIN4ChnIPp5gCeADhCHALYQBcyARHPQNw7Jx0DOYUoA5qwC+WUJFiIUAIUxsCxMhWp16AIxZsVXHvyFYs8lAGFkAXjTIAPskmt9pKXE4ozGYVggAPEgHsoYZAjeINzIwJwAKvaKzsgAPOHInpAgACacyNy8IHwANMgAct4pEAlJ5GmYclFUtBna2YIAfAAUbAbRdOFYAJSmjcjNIEW1hcXh3XRDxaHpAPKUwGCxoyV59FUKNfT9AGSY+ITVSsgJgn0yuFAQYACuUCDIUxAAdBvkNaYmZu01QraBwX8Hk4dEMAG0ALqmZBgjAHH7HRj0PIcBhMZCCPJwt4dBhqPKaPH0DEQ-5BEJETgARmhwOeMGAABsxM0wpFNtRmkjut1npQ4CRmh5zh5nnBuqwgA
interface A {
__typename: "a";
a: string;
}
interface B {
__typename: "b";
b: string;
}
type C = A | B;
type Base = {}
export const isTypename = <T extends string, NodeT extends {__typename: string}>(
typename: T
) => (node: NodeT): node is Omit<NodeT, "__typename"> & { __typename: T } => {
return node.__typename === typename;
};
const xs: C[] = [{ __typename: "a", a: "a" }, { __typename: "b", b: "b" }];
const ys1 = xs.filter(isTypename("a")).map(x => x.a);
Odpoveď č. 1
Myslím, že ak očakávate, že budete používať diskriminované unijné typy, potom dosiahnete lepšie správanie, ak vaša používateľom definovaná ochrana typu vráti typ ako val is Extract<DiscriminatedUnionType, {discriminant: LiteralDiscriminant}> namiesto prienikov ako val is DiscriminatedUnionType & {discriminant: LiteralDiscriminant} (alebo toho s Omit<DiscriminatedUnionType , "discriminant"> & {discriminant: LiteralDiscriminant}, ktorý sa ani nezostaví, pretože kompilátor nemôže overiť, že je to podtyp DiscriminatedUnionType).
AKTUALIZÁCIA PRE TS3.9+: vyššie uvedený problém s prienikmi bol v TS3.9 výrazne zlepšený. Teraz sú prieniky redukované pomocou diskriminačných vlastností (pozri microsoft/TypeScript#36696), takže val is DiscriminatedUnionType & {discriminant: LiteralDiscriminant} teraz eliminuje nekompatibilné typy z diskriminovaného zväzku. Stále však odporúčam použiť Extract, pretože výstup je jednoduchší a nenesie so sebou prienik. KONIEC AKTUALIZÁCIE
Extract<T, U> je pomocný typ, ktorý používa distribuovaný podmienený typ na filtrovanie zjednotení. Hypoteticky by sa dali vytvoriť prieniky, ktoré by robili to isté, ale v TypeScripte prieniky nesúvisiacich objektových typov sa nezredukujú na never. Takže zatiaľ čo {foo: "a", a: string} & {foo: "b"} je ekvivalentné never (nikdy nedostanete hodnotu tohto typu), kompilátor ho v skutočnosti nezredukuje na never. Na druhej strane, Extract<{foo: "a", a: string}, {foo: "b"}> produkuje never. A potrebujete túto redukciu, pretože X | never sa redukuje na X.
Takže vyskúšajme nasledujúcu verziu typovej ochrany:
const isTypename = <T extends string>(typename: T) => <
N extends { __typename: string }
>(
node: N
): node is Extract<N, { __typename: T }> => {
return node.__typename === typename;
};
v ktorom predikát návratového typu používa Extract.
Všimnite si, že som tiež presunul generický N (váš NodeT) do podpisu vracanej funkcie. To by malo pomôcť s odvodzovaním typov. Volanie isTypeName("a") samo o sebe nemá zjavné miesto pre odvodzovanie N. Ak by som urobil
const filterCb = isTypename("a");
xs.filter(filterCb);
Vaša verzia by nakoniec mala N odvodené ako { __typename: string } namiesto C, a bohužiaľ návratový typ by bol node is never namiesto node is A, čo by bol problém.
Niekedy vám kontextové písanie môže zachrániť, takže
xs.filter(isTypename("a"));
mohlo by byť dosť kontextového typu na to, aby sa N odvodilo ako C a všetko by bolo skvelé. Ale všeobecne by som chcel, aby isTypeName("a") vrátilo generickú funkciu, v ktorej N určite nebude predčasne vyriešené.
Dobre, uvidíme, či to funguje:
const ys1 = xs.filter(isTypename("a")).map(x => x.a); // string[]
Vyzerá to dobre! Dúfam, že to pomôže. Veľa šťastia.
Odkaz na kód
