typescript - Mallsträngtyp för en nästlad typ


Givet en tvånivå-nestad typ (aldrig fler, aldrig färre nivåer):

export type SomeNested = {
  someProp: {
    someChild: string
    someOtherChild: string
  }
  someOtherProp: {
    someMoreChildren: string
    whatever: string
    else: string
  }
}

Jag vill ha följande unionstyp: "someProp.someChild" | "someProp.someOtherChild" | "someOtherProp.someMoreChildren" | "someOtherProp.whatever" | "someOtherProp.else".

Jag har försökt något i stil med

type FirstLevel = keyof SomeNested
type SecondLevel = SomeNested[FirstLevel]
type Dot = `${FirstLevel}.${SecondLevel}`

men jag saknar förmodligen någon villkor. Kan någon peka mig i rätt riktning?

Tack!

Svar #1

Observera, denna fråga frågar specifikt efter att få unionen av prickade vägar för en objekttyp som består av exakt två nivåer. För dem som är intresserade av unionen av prickade vägar för ett objekt av godtycklig djup bör du istället titta på denna fråga.


Genom att indexera i SomeNested med unionen av dess kända nycklar type FirstLevel=keyof SomeNested, får du en union av de kända egenskapstyperna:

type X = SomeNested[FirstLevel];
/* type X = {
    someChild: string;
    someOtherChild: string;
} | {
    someMoreChildren: string;
    whatever: string;
    else: string;
} */

Och nycklarna till den föreningen skulle bara vara de nycklar som förekommer i alla medlemmar av den föreningen (vilket skulle vara ingenting alls, för ditt exempel):

type SecondLevel = keyof SomeNested[FirstLevel] // never

I princip genom att skapa dessa unioner har du kastat bort information om korrespondensen mellan specifika nycklar i SomeNested och deras specifika egenskapstyper. För att åtgärda det kommer du i princip att behöva iterera genom nycklarna i SomeNested och titta på underegenskaperna för varje nyckel separat.

Ett sätt att göra detta är med en mappad typ, som låter dig iterera över nycklar och sedan skapa en egenskap för varje nyckel. Till exempel:

type Mapped = { [K in keyof SomeNested]:
  `${K}.${Extract<keyof SomeNested[K], string>}`
}
/* type Mapped = {
    someProp: "someProp.someChild" | "someProp.someOtherChild";
    someOtherProp: "someOtherProp.someMoreChildren" | 
      "someOtherProp.whatever" | "someOtherProp.else";
} */

Varje egenskap av Mapped är en template literal-typ som börjar med en viss nyckel K från keyof SomeNested, följt av en punkt, och sedan följt av unionen av strängnycklar från motsvarande egenskap SomeNested[K]. (I idealfallet skulle du bara kunna skriva $ {keyof SomeNested[K]}, men kompilatorn inser inte att dessa definitivt kommer att vara strängar för alla möjliga K, så vi kan använda hjälp-typen Extract<T, U> för att övertyga kompilatorn om att vi bara tittar på string-kompatibla nycklar).

Självklart vill du inte egentligen ha Mapped, en objekttyp med samma nycklar som SomeNested. Men du vill ha föreningen av dess egenskapstyper, så nu kan du indexera i den med föreningen av dess nycklar:

type Dot = { [K in keyof SomeNested]:
  `${K}.${Extract<keyof SomeNested[K], string>}`
}[keyof SomeNested]
/* type Dot = "someProp.someChild" | "someProp.someOtherChild" | 
  "someOtherProp.someMoreChildren" | "someOtherProp.whatever" | "someOtherProp.else" */

Och där har du det!

Playground-länk till kod