Website-Suche

Kontrolltyp-Ebene „Auswertung“ in Typescript


Ich schreibe eine Open-Source-TypeScript-Bibliothek, die auf NPM veröffentlicht wird. Ich suche nach einigen "Tricks", die meine Bibliothek für ihre Benutzer hilfreicher machen könnten.

Zum Beispiel werden einige Bibliotheksfunktionen einige nicht triviale Typoperationen einschließen. Ein Beispiel ist

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] }

die beabsichtigt, potenziell undefinierte Schlüssel in einem Objekt optional zu machen.

Mein Problem ist, dass dies zu abgeleiteten Typsignaturen wie führt

const myThing: Thing<UndefinedOptional<...>> = ...

wo die UndefinedOptional-Operation in den "User-Space" "leckt". Zu verstehen, was das ist, ist etwas, womit ich Bibliotheksnutzer nicht unnötig belasten möchte.

Was ich stattdessen möchte, ist, die Auswertung dieser Typ-Ebene-Funktion „zu erzwingen“ (ähnlich wie man einen Thunk in der Lazy-Evaluation erzwingen würde) in den Typ zu erzwingen, den UndefinedOptional zurückgeben würde.

Ich bin auch daran interessiert, TypeScript dazu zu bringen, Typen wie { foo: number } & { bar: string } in { foo: number, bar: string } zu evaluieren, wenn es irgendwelche Tricks gibt, die dabei helfen könnten.

Es ist in Ordnung, wenn die Methoden, um dies zu erreichen, ein wenig „hacky“ sind, womit ich zum Beispiel meine, dass man sich auf Verhalten verlässt, das nicht ausdrücklich standardisiert ist, sich zwischen Versionen ändern kann usw.

Hier ist ein Spielplatz mit einigen vereinfachten Beispielen meiner Definitionen

Antwort #1

Mit einem gemappten Typ können wir den Typ in ein Objektliteral „komprimieren“:

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

Dies gibt Ihnen nicht viel Kontrolle über den resultierenden Typ. Zum Beispiel, wenn Sie ein Interface namens MyInterface haben, wird der resultierende Typ MyInterface nicht anzeigen, sondern den tatsächlichen Typ, den MyInterface enthält.

Hier ist ein Spielplatz mit einigen Tests.

Antwort #2

Ich habe diese beiden Typen verwendet. Der erste funktioniert ähnlich wie die andere Lösung, da er rekursiv alle Typalias in tatsächliche Typen auflöst.

Das andere ist ein Versuch, das nicht so tief zu machen und sogar die Aliase zu „zerstören“, die mir wirklich wichtig sind.

/** 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] }

Beachten Sie, dass die Lösung aus dieser ausgezeichneten Antwort stammt https://stackoverflow.com/a/71426138/985454


Es gibt noch zwei weitere Lösungen in diesem TypeScript-Repo-Problem
Vorschlag: Eine Möglichkeit bieten, bedingte und gemappte Typen zu vereinfachen/normalisieren #47980

type Expand<T> = T extends unknown 
  ? { [K in keyof T]: T[K] }
  : never;

Und rekursive Variante

type Expand<T> = T extends unknown
  ? { [K in keyof T]: Expand<T[K]> }
  : never;