javascript - TypeScript: Noen teknikker for å bryte en rekursiv betinget typeavledning?


Jeg prøver å skrive en funksjon som tar et argument som reduserer en serie med elementer til én akkumulert verdi. Her er en forenkling av det jeg har:

// A stub type for the items
interface Item { t: 'item'; }

function paginate<A>(reduce: (acc: A, item: Item, index: number) => A): Promise<A> {
  // ... stub for actual implementation ...
  return true as unknown as Promise<A>;
}

Hvis jeg kaller denne funksjonen, får jeg ikke typen av inferens som jeg ønsker. Jeg håper at når returtypen til reduce er kjent, bør også det første argumentet (acc) bli inferert til å være den typen.

// expected: Promise<number>, inferred: Promise<{}>
const result = paginate((acc, item, index) => {
  acc; // expected: number, inferred: {}
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

Jeg prøvde å bruke betingede typer med inferens for å løse dette problemet, men alle variasjonene jeg har prøvd har enten feilet fordi de ikke begrenser den generiske typeparameteren for acc til noe spesifikt, eller feilet fordi inferensen er rekursiv. Jeg er ikke helt sikker.

type Reducer<A> = (acc: A, item: Item, index: number) => A;
type Accumulator<Reduce> = Reduce extends (acc: infer A, item: Item, index: number) => infer A ? A : never;

// Probably too loose (using an any)
function paginateA<R extends Reducer<any>>(reduce: R): Promise<Accumulator<R>> {
  // ...
  return true as unknown as Promise<Accumulator<R>>;
}

// expected: Promise<number>, inferred: Promise<number>
const resultA = paginateA((acc, item, index) => {
  acc; // expected: number, inferred: any
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

// Probably too recursive (tried to circuit-break with only inferring the return type first)
function paginateB<R extends Reducer<ReturnType<R>>>(reduce: R): Promise<Accumulator<R>> {
  // ...
  return true as unknown as Promise<Accumulator<R>>;
}

// expected: Promise<number>, inferred: Promise<any>
const resultB = paginateB((acc, item, index) => {
  acc; // expected: number, inferred: any
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

Finnes det noen teknikker for å «bryte kretsen» i en rekursiv betinget typeinferens? Jeg ser at Anders nevner at noen klasser av rekursiv inferens er akseptable (selv om hurtiginformasjonen vil vise typen som any), men jeg kan ikke forstå under hvilke betingelser dette skjer.

Er det noen annen teknikk jeg går glipp av? paginateA() ser ut til å fungere best, fordi den i det minste fikk typen til resultA riktig. Er det noen grunn til det?

Her er en lekeplass med all koden ovenfor å manipulere.

Svar #1

Som jeg sa, tror jeg at den spesifikke slutningen av A fra Reducer<A> du prøver å oppnå kan skje automatisk i TypeScript 3.4 og oppover, men det har ikke blitt utgitt ennå. For TypeScript 3.3 og lavere:

Den spesielle teknikken jeg har sett brukt i tilfeller som dette hvor du vil si

declare function f<T extends Something<T>>(x: T): void; // circularity error or other issue

er å la det generiske T være ukonstruert, og plassere begrensningen på funksjonsparameteren via en snitt:

declare function f<T>(x: T & Something<T>): void; // works now

Jeg kan ikke finne kanonisk dokumentasjon for dette, men slik jeg tror dette fungerer er at når du kaller f(x), prøver kompilatoren å utlede T fra x av typen T & Something<T>. Siden x er en T & Something<T>, må det være en T, ut fra hvordan snitt fungerer. Så typen til x-parameteren brukes som T. Deretter vil den sjekke snittet med Something<T> og hvis det ikke fungerer får du en kompilatorfeil.

La oss prøve det i ditt tilfelle, men før vi gjør det, en stor forbehold: du kan sannsynligvis ikke få kompilatoren til både å utlede paginate()'s R typeparameter fra verdien du sender inn som reduce, og utlede parametertypene til verdien du sender inn som reduce fra typen til R som utledes i kallet til paginate(). Det vil si, enten vil R bli utledet som Reducer<number>, men du må da annotere (acc:number, item:Item, index:number). .. eller du må spesifisere R som Reducer<number>, og kompilatoren vil slutte seg til typene til acc, item, index. Du vil ha det på begge måter, men jeg tror ikke kompilatoren er smart nok. Eller i det minste kan jeg ikke få det til å skje. Så jeg kommer for nå til å anta at du prøver å slutte deg til R fra en fullt annotert reduce-callback:

interface Item {
  t: "item";
}

type Reducer<A> = (acc: A, item: Item, index: number) => A;
type Accumulator<Reduce> = Reduce extends Reducer<infer A> ? A : never;

declare function paginateC<R extends Reducer<any>>(
  reduce: R & Reducer<ReturnType<R>> // intersection here
): Promise<Accumulator<R>>;

// Promise<number> as desired
const resultC = paginateC((acc: number, item: Item, index: number) => 5);

// error, string is not number:
paginateC((x: number, y: Item, z: number)=>"string"); 

// okay, since string is a subtype of unknown
paginateC((x: unknown, y: Item, z: number)=>"string")

// also works with any
paginateC((x: any, y: Item, z: number)=>"string")

Så det er greit.

Tilbake til problemet med å utlede acc, item, index... Du kan alltid spesifisere R slik:

// acc, item, index inferred from specified R
paginateC<Reducer<number>>((acc, item, index) => 5);

men du vil ikke gjøre det.

I sannhet, jeg ville ikke forvente at kompilatoren innsnevrer acc-parameteren til ditt ønskede A, siden funksjonsparametere alltid kan utvides trygt (kontravarians av funksjonsparametere). KOMPILATORen vil sannsynligvis la acc være noe bredt som {}, unknown, eller any med mindre du annoterer det eller spesifiserer R manuelt som ovenfor.

Jeg er litt overrasket over at det ikke snevrer item og index ned til henholdsvis Item og number i stedet for å la dem være any. Men det er antagelig ikke mye å gjøre med det, siden det er en kjent begrensning ved typeinferenz at det ikke skjer i flere passeringer. Jaja.

OK, håper det hjelper. Lykke til!

Relaterte artikler