Typescript: inferer typen til generisk etter valgfrie første generiske


Jeg har en funksjon med to generiske typer, In og Out:

function createTask<
  In extends Record<string, any> = {},
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 
// TaskWrapper wraps several other types and interfaces, so args is more than just `In`

Denne koden kompilerer for øyeblikket ikke, fordi du ikke kan ha en påkrevd generisk type (Out) etter en valgfri (In).

Hvordan forteller jeg Typescript-kompilatoren at jeg vil la brukeren av denne funksjonen gjøre en av tre ting:

  1. Ikke spesifiser generiske typer: createTask(...). Typen til In bør standardisere til {}, og Out bør utledes fra returverdien til TaskFunction.

  2. Spesifiser bare In: createTask<A>(...). Som ovenfor, Out bør utledes.

  3. Spesifiser både In og Out: createTask<A, B>(...).

I hovedsak leter jeg etter en måte å si "dette generiske er valgfritt og bør utledes" på. Jeg vet at det finnes et infer-nøkkelord, men ut fra den begrensede dokumentasjonen jeg har funnet om det, ser det ikke ut til å støtte denne bruken.

Jeg har også prøvd å tilordne en standardverdi til Out, men da bruker den alltid den standardverdien i stedet for å utlede fra TaskFunction.

Jeg kan reversere rekkefølgen på In og Out, men da må Out alltid spesifiseres selv om det enkelt kan utledes, hvis brukeren vil spesifisere In.

Jeg foretrekker også å ikke tvinge brukere til å legge til standardverdien {} hver gang de kaller funksjonen.

Er dette overhodet mulig å gjøre med Typescript, eller må jeg alltid kreve at In blir spesifisert?

Svar #1

Du ønsker noe som delvis typeparameter-inferens, noe som for øyeblikket ikke er en funksjon i TypeScript (se microsoft/TypeScript#26242). Akkurat nå må du enten spesifisere alle typeparametere manuelt eller la kompilatoren inferere alle typeparametere; det finnes ingen delvis inferens. Som du har lagt merke til, tilfredsstiller ikke standardverdier for generiske typeparametere dette behovet; en standard slår av inferens.

Så det finnes løsninger. De som fungerer konsekvent, men som også er noe irriterende å bruke, er enten currying eller «dummying». Currying her betyr at du deler den enkelt funksjonen med flere typer argumenter inn i flere funksjoner med ett typeargument:

type Obj = Record<string, any>; // save keystrokes later

declare const createTaskCurry:
    <I extends Obj = {}>() => <O extends Obj>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskCurry()(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskCurry<{ bar: number }>()(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskCurry<{ bar: number }>()<{ foo: string, baz?: number }>(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

Du har nøyaktig den oppførselen du ønsker med hensyn til dine I og O-typer, men det er denne irriterende utsette funksjonskallet i veien.

Her betyr 'dummying' at du gir funksjonen en dummy-parameter av de typene du ønsker å spesifisere manuelt, og lar inferens ta plassen til manuell spesifikasjon:

declare const createTaskDummy:
    <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O & {}>, 
      i?: I, o?: O) => Task<I, O>;

createTaskDummy(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number });
// I is {bar: number}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number }, 
  null! as { foo: string, baz?: number });
// I is {bar: number}, O is {foo: string, baz?: number}

Igjen, du har den oppførselen du ønsker, men du sender inn tull-/dummyverdier til funksjonen.

Selvfølgelig, hvis du allerede har parametere av riktig type, bør du ikke trenge å legge til en "dummy"-parameter. I ditt tilfelle kan du absolutt gi nok informasjon i task-parameteren for at kompilatoren skal kunne utlede I og O, ved å annotere eller på annen måte spesifisere typene inne i task-parameteren:

declare const createTaskAnnotate: 
  <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskAnnotate(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskAnnotate((a: { bar: number }) => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskAnnotate((a: { bar: number }): { foo: string, baz?: number } => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

Dette er sannsynligvis løsningen jeg ville anbefalt her, og er i praksis det samme som det andre svaret som ble lagt ut. Så alt dette svaret gjør er å grundig forklare hvorfor det du vil gjøre ikke er mulig for øyeblikket og hvorfor de tilgjengelige løsningene leder deg bort fra det. Jaja!

OK, håper det hjelper å få klarhet i situasjonen. Lykke til!

Lenke til kode på lekeplassen

Svar #2

Først, fjern standardtypen helt:

declare function createTask<
    In extends Record<string, any>,
    Out extends Record<string, any>,
    >(task: TaskFunction<In, Out>): Task<In, Out>;

For tilfelle du beskriver, hvor In blir sendt:

const r1 = createTask<{ a : number }>(arg => {
    return { b: arg.a };
}); // Error: Expected 2 type arguments, but got 1.

Ikke send det som en type-parameter. Merk verdien du vil begrense, og la det gjette resten av typene:

const r1 = createTask((arg: { a: number }) => {
    return { b: arg.a };
}); // r1 is Task<{a: number;}, {b: number;}>

Dette fungerer også når alle typene er kjent:

declare function foo(arg: { a: number }): { b: boolean };

const r1 = createTask(foo); // r1 is Task<{a: number;}, { b: boolean;}>

Jeg prøvde å legge til TaskWrapper som du angav i endringene dine. Løsningen virker identisk.

type Task<In, Out> = { a: In, b: Out}
type TaskWrapper<T> = { a: T }
type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 

declare function createTask<
  In extends Record<string, any>,
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

const r1 = createTask((args: TaskWrapper<{ a: number }>) => {
    return { b: args.a.a };
}); // r1 is Task<{a: number;}, {b: number;}>

Relaterte artikler