typescript - Erstatt generisk grensesnitt-typeparameter
Jeg prøver å lage et generisk funksjonsgrensesnitt for funktor map, som respekterer grensesnittet som er gitt. I koden vist nedenfor, vil jeg at verdien av mb skal være av typen Maybe<number>, i motsetning til den faktiske typen Functor<number>.
Jeg er klar over at en mulig løsning er å legge til en overbelastning i grensesnittet FMap. Grunnen til at jeg ikke er fornøyd med denne løsningen, er at jeg ønsker at denne koden skal ligge i en pakke, slik at brukere kan lage implementasjoner for Functor, og ha oppførselen jeg beskrev ovenfor når de bruker funksjonen map.
interface Functor<A> {
map<B>(fn: (a: A) => B): Functor<B>;
}
interface FMap {
<A, B>(fn: (a: A) => B, Fa: Functor<A>): Functor<B>;
}
const map: FMap = (fn, Fa) => (
Fa.map(fn)
);
class Maybe<A> implements Functor<A> {
constructor(private readonly a: A) {}
map<B>(fn: (a: A) => B): Maybe<B> {
return new Maybe<B>(fn(this.a));
}
}
const sqr = (x: number) => x*x;
const ma = new Maybe(5);
const mb = map(sqr, ma);
Jeg ønsker noen måter å uttrykke følgende semantikk på:
// Theoretical Code
interface PretendFMap {
<A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never;
}
Dette fungerer imidlertid ikke, ettersom et generisk grensesnitt uten en typeparameter ikke er en gyldig TypeScript-type, altså et grensesnitt som Functor krever en typeparameter for å bli betraktet som en type, Functor i seg selv er ikke en gyldig type.
Hvis det for øyeblikket ikke finnes noen måter å uttrykke disse semantikkene på, vil ethvert forslag til en løsning som krever så lite kode som mulig fra brukerens side bli høyt verdsatt.
Takk på forhånd for din tid og omtanke.
Svar #1
Det som står i veien for oss, er når du prøver å sende en typevariabel F som typeparameter til en annen typevariabel T, som T<F>, tillater ikke TS det, selv om du vet at T faktisk er et generisk grensesnitt.
Det er en diskusjon om dette emnet som går tilbake til 2014 i en GitHub-issue, og den er fortsatt åpen, så TS-teamet vil sannsynligvis ikke støtte det i nær fremtid.
Begrepet for denne språkfunksjonen kalles higher kinded type. Ved å bruke det søkeordet tok Google meg med på en reise ned kaninhullet.
Det viser seg at det finnes en veldig smart løsning!
Ved å utnytte TS deklarasjonssammenslåing (også kjent som modulforsterkning) funksjonen, kan vi effektivt definere et tomt "typelager"-grensesnitt, som fungerer som et enkelt objekt som holder referanse til andre nyttige typer. Ved å bruke denne teknikken, er vi i stand til å overvinne dette hinderet!
Jeg vil bruke din sak som eksempel for å forklare ideen bak denne teknikken. Hvis du vil fordype deg mer, inkluderer jeg noen nyttige lenker på slutten.
Her er TS Playground-lenken (spoiler alert) til det endelige resultatet. Se det live for sikkerhets skyld. Nå la oss bryte det ned (eller burde jeg si bygge det opp?) steg for steg.
- Først, la oss erklære et tomt
TypeStore-grensesnitt, vi vil oppdatere innholdet senere.
// just think of it as a plain object
interface TypeStore<A> { } // why '<A>'? see below
// example of "declaration merging"
// it's not re-declaring the same interface
// but just adding new members to the interface
// so we can amend-update the interface dynamically
interface TypeStore<A> {
Foo: Whatever<A>;
Maybe: Maybe<A>;
}
- La oss også få
keyof TypeStore. Merk at når innholdet iTypeStoreoppdateres, oppdateres også$keystilsvarende.
type $keys = keyof TypeStore<any>
- Nå fikser vi den manglende språkfunksjonen "higher kinded type" ved å bruke en hjelpe-type.
// the '$' generic param is not just `string` but `string literal`
// think of it as a unique symbol
type HKT<$ extends $keys, A> = TypeStore<A>[$]
// where we mean `Maybe<A>`
// we can instead use:
HKT<'Maybe', A> // again, 'Maybe' is not string type, it's string literal
- Nå som vi har de riktige verktøyene, la oss begynne å lage nyttige ting.
interface Functor<$ extends $keys, A> {
map<B>(f: (a: A) => B): HKT<$, B>
}
class Maybe<A> implements Functor<'Maybe', A> {
constructor(private readonly a: A) {}
map<B>(f: (a: A) => B): HKT<'Maybe', B> {
return new Maybe(f(this.a));
}
}
// HERE's the key!
// You put the freshly declare class back into `TypeStore`
// and give it a string literal key 'Maybe'
interface TypeStore<A> {
Maybe: Maybe<A>
}
- Endelig
FMap:
// `infer $` is the key here
// remember what blocked us?
// we cannot "infer Maybe from T" then apply "Maybe<A>"
// but we can "infer $" then apply "HKT<$, A>"!
interface FMap {
<A, B, FA extends { map: Function }>
(f: (a: A) => B, fa: FA): FA extends HKT<infer $, A> ? HKT<$, B> : any
}
const map: FMap = (fn, Fa) => Fa.map(fn);
Referanse
- Diskusjonen på GitHub om støtte for høyereordnet typer i TS
- Inngang til kaninhullet
- Erklæringssamordning i TS-håndboken
- SO-innlegg om høyere ordnet type
- Medium-innlegg av @gcanti, om høyere ordnede typer i TS
fp-tsbibliotek av @gcantihktsbibliotek av @pelotomtypepropsbibliotek av @SimonMeskens
