TypeScript-generics fungerer når du kaller den, men ikke i funksjonskroppen?


Si jeg har dette eksempelet:

type Keys = 'one' | 'two';
type Values<T extends Keys> = T extends 'one' 
  ? { id: 1, name: 'hello1' } 
  : T extends 'two' 
    ? { id: 2, value: 'hello2' }
      : never;

type Fn = <T extends Keys>(key: T) => (value: Values<T>) => void;

const fn: Fn = (key) => (value) => {
  if (key === 'one') {
    const name = value.name; // This throws compile error
  }
}

// Both of these `fn` calls correctly limit the `value` argument based on the `key`
fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });

Kan noen forklare for meg hvorfor denne oppsettet korrekt begrenser de tillatte typene for value-argumentet når man kaller fn-metoden, men ikke lar meg snevre inn value etter å ha brukt en Type Guard inne i fn-implementasjonen?

Lekeplass - Jeg har denne lekeplassen med noen andre forsøk på å få dette til å fungere, men uten suksess.

Til syvende og sist prøver jeg å bruke noe lignende dette som en useCallback-metode i et React-prosjekt hvor en onChange-prop settes med callbacken som kalles med en nøkkel, og deretter sender hendelsen verdien. Siden jeg kjenner typene til hendelsen, føler jeg at dette burde være mulig. Eks:

<Component1 onChange={handleChange('key1')} />
<Component2 onChange={handleChange('key2')} />

Svar #1

Først la oss kvitte oss med din betingede typedefinisjon av Values<K>. Dette kan enkelt refaktoreres til et indeksert oppslag i et kartleggingsgrensesnitt:

interface KeyVals {
  one: { id: 1, name: "hello1" },
  two: { id: 2, value: "hello2" }
}
type Keys = keyof KeyVals;
type Values<K extends Keys> = KeyVals[K];

Kompilatoren er litt bedre til å analysere indekserte tilganger enn den er til å analysere betingede typer. Dette løser ikke problemet ditt, men det gjør at løsningen min fungerer bedre, så jeg vil gjøre det.


Hovedproblemet her er misforholdet mellom hvordan kompilatoren håndterer diskriminerte unioner og hvordan den håndterer generics. Din fn()-funksjonskallsignatur er generisk, med verdier k av typen K og v av typen Values<K>, men implementeringen av funksjonen din må behandle disse som diskriminerte unioner, der sjekking av verdien til k skal begrense typen til v. Men å sjekke k begrenser kun typen til k, og begrenser ikke type-parameteren K. Så du kan ikke behandle disse generiske verdiene som diskriminerte unioner.

Dette er en slags generell begrensning i TypeScript for øyeblikket, med ulike problemer på GitHub som er rapportert om forskjellige aspekter av det. For eksempel, det finnes microsoft/TypeScript#33014, som eksplisitt ber om en sjekk på k for å avgrense typeparameteren K. Dette problemet er for øyeblikket åpent. Det vil sannsynligvis også trenge microsoft/TypeScript#27808 implementert, slik at du kan forhindre at folk passerer inn en union for K, siden det skaper problemer. Det finnes også microsoft/TypeScript#30581 som spør om hvordan man skal behandle unioner generisk, og den anbefalte løsningen/tilnærmingen til det på microsoft/TypeScript#47109 er i hovedsak å omforme unioner som generiske ... men her trenger du at unionadferden fungerer. Akkurat nå ser det ut til at dette rett og slett ikke er mulig.


Du prøvde også tilnærmingen med å gjøre fn() til en ikke-generisk overbelastet funksjon. Dette ville unngå generics helt. Dessverre er kompilatoren enda mindre i stand til å analysere overbelastninger enn den er med generics. Inne i implementasjonen av en overbelastet funksjon statement begrenser ikke kompilatoren ting til kun å være kompatible med kall-signaturene; den ser bare på implementasjons-signaturen, og den matcher implementasjons- og kall-signaturer veldig løst. Se microsoft/TypeScript#13235 for en avvist forespørsel om å få overbelastninger sjekket på en typesikker måte. Og hvis du bruker et overbelastet funksjons-expression, da er kompilatoren for streng. Se microsoft/TypeScript#47669 for en fremdeles åpen forespørsel om å gjøre noe annerledes her. Så overbelastninger kan enten gjøres for å undertrykke feil (men tillate noen usikre ting) eller fange feil (men forhindre noen sikre ting), men ikke begge deler.


Så vidt jeg vet, finnes det ingen måte å refaktorere koden din på slik at kompilatoren tillater deg å gjøre det riktige samtidig som den stopper deg fra å gjøre det gale. Alt jeg kan tenke på, er en løsning.

I dette tilfellet er det nærmeste jeg kan komme til nyttig sikkerhet å bruke en funksjonsoverlastingssetning for å bygge bro over gapet mellom generiske typer og diskriminerte unions. Det ser slik ut:

function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {
  if (k === 'one') {
    const name = v.name;
  }
}

Så uncurriedFn() er en enkelt funksjon som tar både k og v i stedet for den curried-versjonen som tar k og returnerer en funksjon som tar v. Funksjons-signaturen til funksjonen er generell, så du bør kunne kalle den trygt som en generell funksjon. Faktisk lar det deg bruke den inne i din tiltenkte curried-versjon:

type Fn = <K extends Keys>(k: K) => (v: Values<K>) => void;
const fn: Fn = (k) => (v) => uncurriedFn(k, v)    

fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });

Så det er kall-signaturen. Hva med implementeringen? Vel, implementeringssignaturen er ikke-generisk og tar et destrukturert diskriminert unionargument. Typen {[K in Keys]: [k: K, v: Values<K>]}[Keys] er et distribuerende objekt (som nevnt i ms/TS#47109) som evalueres til [k: 'one', v: Values<'one'>] | [k: 'two', v: Values<'two'>]. Dette tillater også bare sikre kall; det vil klage hvis du mismatcher k og v. Men nå kan implementeringskroppen bruke k og v som en destrukturert diskriminert union; sjekk k, og v blir innsnevret.


Og der har du det. Alt er faktisk trygt (eller så trygt som rimelig... generics kan noen ganger brukes på en utrygg måte), og kompilatoren klager ikke. Trikset er, igjen, dette paret av kall- og implementeringssignaturer:

function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {}

I en ideell verden ville kompilatoren vite at signaturen <K extends Keys>(k: K, v: Values<K>) => void og (...args: {[K in Keys]: [k: K, v: Values<K>]}[Keys]) => void er kompatible (med forbehold om ms/TS#27808). Men den vet ikke dette, så du må lure den ved å dra nytte av løsheten i sjekken av overbelastningsuttalelser.

Lenke til kode på lekeplassen

Relaterte artikler