typescript - Hvordan kan jeg skrive en generell TypeGuard for GraphQL-unions?


Jeg vil gjerne skrive en universell Type Guard for GraphQL-unioner.

La oss si at vi har to typer A og B, og C er samlingstypen av dem. A har en egenskap a, men B har det ikke. Jeg tror det kan være nyttig hvis vi kunne ha en typevern som dette:

xs.filter(isTypename("a")).map(x => x.a)

Jeg prøvde en stund, men jeg fullførte fortsatt ikke.

Er det mulig å oppnå?

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

Svar #1

Jeg tror at hvis du forventer å bruke diskriminerte unionstyper, så vil du få bedre oppførsel hvis din brukerdefinerte typevakt returnerer en type som val is Extract<DiscriminatedUnionType, {discriminant: LiteralDiscriminant}> i stedet for snitt som val is DiscriminatedUnionType & {discriminant: LiteralDiscriminant} (eller den med Omit<DiscriminatedUnionType , "discriminant"> & {discriminant: LiteralDiscriminant}, som ikke engang vil kompilere fordi kompilatoren ikke kan verifisere at det er en underklasse av DiscriminatedUnionType).

OPPDATERING FOR TS3.9+: problemet ovenfor med krysningspunkter har blitt forbedret betydelig i TS3.9. Nå reduseres krysningspunkter ved hjelp av diskriminant-egenskaper (se microsoft/TypeScript#36696), så val is DiscriminatedUnionType & {discriminant: LiteralDiscriminant} vil nå eliminere inkompatible typer fra den diskriminerte unionen. Jeg anbefaler fortsatt å bruke Extract, fordi resultatet da blir enklere og ikke inkluderer krysningspunktet. SLUTT OPPDATERING

Extract<T, U> er en hjelpetyper som bruker en distribuerbar betinget type for å filtrere forenede typer. Hypotetisk sett kunne snitt lages for å gjøre det samme, men i TypeScript reduseres ikke snitt av separate objekttyper til never. Så selv om {foo: "a", a: string} & {foo: "b"} er ekvivalent med never (du vil aldri få en verdi av den typen), reduserer kompilatoren det ikke faktisk til never. På den annen side vil Extract<{foo: "a", a: string}, {foo: "b"}> produsere never. Og du trenger denne reduksjonen, siden X | never reduseres til X.


Så la oss prøve følgende versjon av typevakten:

const isTypename = <T extends string>(typename: T) => <
  N extends { __typename: string }
>(
  node: N
): node is Extract<N, { __typename: T }> => {
  return node.__typename === typename;
};

der returtypen predikat bruker Extract.

Merk at jeg også flyttet den generiske N (din NodeT) til signaturen til den returnerte funksjonen. Det burde hjelpe med typeimplisitthet. Kallet isTypeName("a") i seg selv har ingen åpenbar implisitt type for N. Hvis jeg gjorde

const filterCb = isTypename("a"); 
xs.filter(filterCb); 

Din versjon ville ende opp med å ha N utledet som { __typename: string } i stedet for som C, og dessverre ville returtypen være node is never i stedet for node is A, noe som ville være et problem.

Noen ganger kan kontekstuelt skriving redde deg, så

xs.filter(isTypename("a"));

kan være nok kontekstuell type til å slutte at N er C og alt ville være flott. Men generelt vil jeg at isTypeName("a") skal returnere en generell funksjon der N definitivt ikke blir løst for tidlig.


OK, la oss se om det fungerer:

const ys1 = xs.filter(isTypename("a")).map(x => x.a); // string[]

Ser bra ut! Håper det hjelper. Lykke til.

Lenke til kode

Relaterte artikler