Website-Suche

typescript - keyof verschachtelte Kindobjekte


Ich habe ein rekursiv typisiertes Objekt, von dem ich die Schlüssel und alle Kindschlüssel eines bestimmten Typs erhalten möchte.

Zum Beispiel. Unten möchte ich einen Vereinigungstyp von erhalten:

'/another' | '/parent' | '/child'

Beispiel :

export interface RouteEntry {
    readonly name: string,
    readonly nested : RouteList | null
}
export interface RouteList {
    readonly [key : string] : RouteEntry
}

export const list : RouteList = {
    '/parent': {
        name: 'parentTitle',
        nested: {
            '/child': {
                name: 'child',
                nested: null,
            },
        },
    },
    '/another': {
        name: 'anotherTitle',
        nested: null
    },
}

In TypeScript können Sie keyof typeof RouteList verwenden, um den Vereinigungs-Typ zu erhalten:

'/another' | '/parent' 

Gibt es eine Methode, um auch die verschachtelten Typen einzubeziehen

Antwort #1

Hier ist eine unendlich rekursive Lösung:

type Paths<T> = T extends RouteList
  ? keyof T | { [K in keyof T]: Paths<T[K]['nested']> }[keyof T]
  : never

type ListPaths = Paths<typeof list> // -> "/parent" | "/another" | "/child"

Getestet mit TypeScript v3.5.1. Außerdem müssen Sie die Typanmerkung der list-Variablen entfernen, wie von @jcalz empfohlen.

Antwort #2

Das ist eine harte Nuss. TypeScript fehlt sowohl die gemappten bedingten Typen als auch allgemeine rekursive Typdefinitionen, die ich beide verwenden würde, um Ihnen diesen Union-Typ zu geben. (Bearbeitung 2019-04-05: Bedingte Typen wurden in TS2.8 eingeführt) Es gibt einige Stolpersteine bei dem, was Sie wollen:

  • Die nested-Eigenschaft eines RouteEntry kann manchmal null sein, und Typausdrücke, die zu keyof null oder null[keyof null] ausgewertet werden, beginnen, Probleme zu verursachen. Man muss vorsichtig sein. Mein Workaround besteht darin, einen Dummy-Schlüssel hinzuzufügen, damit er nie null ist, und ihn dann am Ende wieder zu entfernen.
  • Egal welchen Typ-Alias Sie verwenden (nennen Sie ihn RouteListNestedKeys<X>), er scheint in Bezug auf sich selbst definiert werden zu müssen, und Sie werden einen „kreislaufbezogenen Verweis“-Fehler erhalten. Eine Umgehung wäre, etwas bereitzustellen, das bis zu einem bestimmten endlichen Verschachtelungsgrad funktioniert (z. B. 9 Ebenen tief). Dies könnte den Compiler erheblich verlangsamen, da er möglicherweise alle 9 Ebenen sofort auswertet, anstatt die Auswertung auf später zu verschieben.
  • Dies erfordert eine Menge Typalias-Komposition unter Verwendung von abgebildeten Typen, und es gibt einen Fehler bei der Komposition abgebildeter Typen, der erst bis TypeScript 2.6 behoben wird. Eine Umgehungslösung besteht darin, generische Standardtypparameter zu verwenden.
  • Der Schritt „Am Ende einen Dummy-Schlüssel entfernen“ beinhaltet eine Typoperation namens Diff, die mindestens TypeScript 2.4 benötigt, um korrekt ausgeführt zu werden.

All das bedeutet: Ich habe eine Lösung, die funktioniert, aber ich warne dich, sie ist komplex und verrückt. Noch eine letzte Sache, bevor ich den Code einfüge: du musst ändern

export const list: RouteList = { // ...

zu

export const list = { // ...

Das heißt, entfernen Sie die Typannotation von der list-Variable. Wenn Sie sie als RouteList angeben, verlieren Sie TypeScripts Wissen über die genaue Struktur von list, und Sie erhalten nichts anderes als string als Schlüsseltitel. Wenn Sie die Annotation weglassen, lässt Sie TypeScript den Typ ableiten, und es wird sich daher an die gesamte verschachtelte Struktur erinnern.

Okay, hier geht's:

type EmptyRouteList = {[K in 'remove_this_value']: RouteEntry};
type ValueOf<T> = T[keyof T];
type Diff<T extends string, U extends string> = ({[K in T]: K} &
  {[K in U]: never} & { [K: string]: never })[T];
type N0<X extends RouteList> = keyof X
type N1<X extends RouteList, Y = {[K in keyof X]: N0<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N2<X extends RouteList, Y = {[K in keyof X]: N1<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N3<X extends RouteList, Y = {[K in keyof X]: N2<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N4<X extends RouteList, Y = {[K in keyof X]: N3<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N5<X extends RouteList, Y = {[K in keyof X]: N4<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N6<X extends RouteList, Y = {[K in keyof X]: N5<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N7<X extends RouteList, Y = {[K in keyof X]: N6<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N8<X extends RouteList, Y = {[K in keyof X]: N7<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type N9<X extends RouteList, Y = {[K in keyof X]: N8<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
type RouteListNestedKeys<X extends RouteList, Y = Diff<N9<X>,'remove_this_value'>> = Y;

Lass es uns ausprobieren:

export const list = {
    '/parent': {
        name: 'parentTitle',
        nested: {
            '/child': {
                name: 'child',
                nested: null,
            },
        },
    },
    '/another': {
        name: 'anotherTitle',
        nested: null
    },
}

type ListNestedKeys = RouteListNestedKeys<typeof list> 

Wenn Sie ListNestedKeys überprüfen, werden Sie sehen, dass es "parent" | "another" | "child" ist, wie Sie es wollten. Es liegt an Ihnen, ob es das wert war oder nicht.

Puh! Hoffentlich hilft das. Viel Glück!

Verwandte Artikel