Rozwiązanie z komentarzem
Opublikowano 29 sierpnia 2026Rozwiązanie
type Paths<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? K | `${K}.${Paths<T[K]> & string}` : K }[keyof T & string] : never
Najczęstsza błędna odpowiedź
type Paths<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}.${Paths<T[K]> & string}` : K }[keyof T & string] : never
To daje tylko liście: `"db.host" | "db.port" | "debug"`. Gałąź dla klucza o wartości obiektowej zwraca potomków z przedrostkiem, ale zapomina, że sam klucz też jest poprawną ścieżką. To `K |` odpowiada za całe pojawienie się ścieżek pośrednich.
Linia po linii
}[keyof T & string]Typ mapowany to rusztowanie, nigdy odpowiedź. Zindeksowanie go wszystkimi własnymi kluczami odczytuje naraz każdą wartość i łączy je w unię — standardowy sposób zamiany obliczenia per-klucz w unię.
Paths<T[K]> & stringTo przecięcie nie jest ozdobą. Literał szablonowy wstawi tylko coś, o czym kompilator już wie, że jest łańcuchopodobne, a odroczone wywołanie rekurencyjne takie nie jest — `& string` to obietnica, która je przepuszcza.
keyof T & stringKlucze obiektu mogą być symbolami, które nie mogą wystąpić w ścieżce tekstowej. Przecięcie ze `string` odsiewa je zarówno przy mapowaniu, jak i przy indeksowaniu — a oba miejsca muszą się zgadzać, bo inaczej wynik się zapada.
Do zapamiętania
Typ mapowany zindeksowany własnymi kluczami to odpowiednik `Object.values().flat()` na poziomie typów. W połączeniu z rekurencją przechodzi dowolnie głębokie struktury — i dokładnie na tym kształcie zbudowane są typowane biblioteki formularzy i walidatory kluczy i18n.
