funktionell programmering - TypeScript generisk curryad funktion med indextyp
Jag har skapat en TypeScript-curried funktion som först tar emot ett egenskapsnamn som en sträng och sedan objektet för att få värdet på den egenskapen från. Jag har använt indextyp för att se till att jag får ett fel när jag försöker komma åt en icke-existerande egenskap:
export interface Dict {
name: string;
age: number;
}
const prop = <T extends Dict, K extends keyof T>(p: K) => (obj: T): T[K] => obj[p];
prop('name')({name: 'John', age: 45}); // John
prop('name2')({name: 'John', age: 45}); // error...
Den sista raden ger felet:
error TS2345: Argument of type '"name2"' is not assignable to parameter of type '"name" | "age"'.
vilket är precis vad jag vill eftersom egenskapen name2 inte existerar på objektet som ges som andra argumentet.
Men när jag försöker skapa en säker version med en Maybe-monad ger det ett liknande fel:
const safeProp = <T extends Dict, K extends keyof T>(p: K) => (obj: T): Maybe<{}> => compose2(Maybe.of, prop(p))(obj);
Felet finns på det andra argumentet i compose2-funktionen: prop(p):
Argument of type 'K' is not assignable to parameter of type '"name" | "age"'.
Vilket jag inte förstår eftersom jag har deklarerat K extends keyof T vilket jag antog var korrekt eftersom det också fungerar för prop-funktionen.
Som referens, compose2-funktionen:
const compose2 = <A, B, C>(f: (b: B) => C, g: (a: A) => B): ((a: A) => C) => a => f(g(a));
och den relevanta delen av Maybe-monaden:
class Maybe<A> {
static of<A>(x: A): Maybe<A> {
return new Maybe(x);
}
...
}
Hur skriver jag korrekt typen för safeProp-funktionen och varför behöver jag specificera dess returtyp som Maybe<{}> och inte Maybe<T[K]>?
Svar #1
const safeProp = <T extends Dict, K extends keyof Dict>(p: K) => (obj: T): Maybe<{}> => compose2(Maybe.of, prop(p))(obj);
Koden ovan ger dig inget fel och jag ska försöka förklara varför (och varför din kod gör det).
K extends keyof Dict är exakt name eller age (vilka är de enda tillåtna parametrarna för funktionen prop). Och K extends keyof T kan verkligen vara vilken som helst nyckel av typen number | string | Symbol. För om T extends Dict, betyder det inte att den inte kan ha några andra nycklar som t.ex. "lastname" osv. Med andra ord:
type IsDict = {name: string, age: number, lastname: string} extends Dict ? true : false // IsDict is true
Ser du? Din kod skulle tillåta lastname-nyckeln, och Typescript skyddar dig från att göra det.
Det sagt, har TypeScript för närvarande inte möjligheten att begränsa en typparameter till exakt typ (det finns lösningar, men tyvärr passar de inte ditt fall).
Svar #2
Det finns ett problem här som beskriver hur Typescript inte korrekt härleder typer när man skickar generiska funktioner till generiska högre ordningsfunktioner (i ditt fall är det att skicka Maybe.of<A> till compose2<A,B,C>).
Du kan lindra detta genom att manuellt fylla i typerna för compose2 när du använder det.
const safeProp = <T extends Dict, K extends keyof T>(p: K) => (obj: T) => compose2<T, T[K], Maybe<T[K]>>(Maybe.of, prop(p))(obj);
safeProp har nu rätt typ const safeProp: <T extends Dict, K extends keyof T>(p: K) => (obj: T) => Maybe<T[K]>.
