Controlați evaluarea la nivel de tip în Typescript
Scriu o bibliotecă open source în TypeScript care va fi publicată pe NPM. Caut câteva „trucuri” care ar putea face biblioteca mea mai utilă pentru utilizatorii ei.
De exemplu, unele funcții din bibliotecă vor include unele operații de tip non-triviale. Un exemplu este
export type UndefinedOptional<A> =
{ [P in keyof A]?: A[P] } &
{ [L in { [K in keyof A]: undefined extends A[K] ? never : K }[keyof A]]: A[L] }
care intenționează să facă cheile potențial nedefinite dintr-un obiect opționale.
Problema mea este că acest lucru duce la semnături de tip inferate precum
const myThing: Thing<UndefinedOptional<...>> = ...
unde operațiunea UndefinedOptional „se scurge” în „spațiul utilizatorului”. Înțelegerea a ceea ce este aceasta este ceva cu care aș prefera să nu împovărez utilizatorii bibliotecii fără necesitate.
Ceea ce aș dori în schimb este să „forțez” evaluarea acestei funcții la nivel de tip (similar modul în care ai forța un thunk într-o evaluare leneșă) în tipul pe care UndefinedOptional l-ar returna.
Sunt, de asemenea, interesat să forțez TypeScript să evalueze tipuri precum { foo: number } & { bar: string } în { foo: number, bar: string }, dacă există vreun truc care ar putea ajuta cu asta.
Este în regulă dacă metodele pentru a realiza acest lucru sunt puțin „neconvenționale”, prin care mă refer, de exemplu, la bazarea pe comportamente care nu sunt explicit standardizate, care pot să se schimbe între versiuni, etc.
Iată un loc de joacă cu câteva exemple simplificate ale definițiilor mele
Răspuns #1
Folosind un tip mapat, putem „comprima” tipul într-un literal de obiect:
type Expand<T> =
// if T is an array
T extends ReadonlyArray<unknown>
? number extends T["length"] // if T is not a tuple
? Expand<T[number]>[] // expand the element type and wrap it back into an array
: { [K in keyof T]: Expand<T[K]> } // T is a tuple
: T extends object // if it's not primitive
? { [K in keyof T]: Expand<T[K]> } // expand it recursively
: T; // T is a primitive so leave as is
Acest lucru nu îți oferă mult control asupra tipului rezultat. De exemplu, dacă ai o interfață numită MyInterface, tipul rezultat nu va afișa MyInterface, ci tipul efectiv pe care MyInterface îl deține.
Iată un loc de joacă cu câteva teste.
Răspunsul #2
Am folosit aceste două tipuri. Primul funcționează similar cu cealaltă soluție, deoarece rezolvă recursiv orice alias de tip în tipuri reale.
Celălalt este o încercare de a nu face asta atât de profund și de a „distruge” chiar și acele pseudonime de care îmi pasă.
/** Normalize type by recursively applying any type aliases and merging explicit intersections. */
type Normalize<T> =
T extends (...args: infer A) => infer R
? (...args: Normalize<A>) => Normalize<R>
: { [K in keyof T]: Normalize<T[K]> }
/** Merges intersection `{a} & {b}` into `{a, b}`. *(Similar to `Normalize` but only one level deep.)* */
type ShallowNormalize<T> = { [K in keyof T]: T[K] }
Rețineți că soluția provine din acest răspuns excelent https://stackoverflow.com/a/71426138/985454
Există alte două soluții în această problemă a depozitului TypeScript
Sugestie: Oferiți o modalitate de a forța simplificarea/normalizarea tipurilor condiționale și mapate #47980
type Expand<T> = T extends unknown
? { [K in keyof T]: T[K] }
: never;
Și varianta recursivă
type Expand<T> = T extends unknown
? { [K in keyof T]: Expand<T[K]> }
: never;
