typescript - Typ för en punktseparerad sökväg till typ
Jag vill starkt typa en punktseparerad väg till en specifik typ. Låt oss säga att vi har en rekursiv struktur där varje blad är en specifik typ, i det här fallet översättningar av en uppsättning språk:
type Language = "pl" | "en";
type Translation = { [ lang in Language ]: string };
type Translations = { [key: string]: Translation | Translations | undefined };
Ett exempel på detta kan vara:
const translations = {
hello: {
pl: "Dzieńdobry",
en: "Hello",
},
bool: {
yes: {
pl: "Tak",
en: "Yes",
},
no: {
pl: "Nie",
en: "No",
},
},
};
Jag vill ha en typ för sökvägarna: "hello", "bool.yes" och "bool.no", men inte för "bool" eller "missing" eller "bool.foo" eller "hello.pl"`. Här är vad jag har hittills:
För en fil med enkellageröversättningar:
type KeyToTranslation<T extends Translations, K extends string = string> = K extends keyof T
? T[K] extends Translation
? K
: never
: never;
function printTranslationByKey<T extends Translations, K extends string>(
t: T,
k: KeyToTranslation<T, K>
) {
console.log(t[k]);
}
printTranslationByKey(translations, "hello"); // Valid, Correct!
printTranslationByKey(translations, "hello.pl"); // Error, Correct!
printTranslationByKey(translations, "missing"); // Error, Correct!
printTranslationByKey(translations, "bool.yes"); // Error, Incorrect.
Så vi behöver några mallsträngar och infers. Tyvärr verkar det som att jag inte kan få det att fungera. Det verkar glömma att jag redan har hävdat att T[TKey] extends Translations:
type DeepKeyToTranslation<T extends Translations, K extends string = string> = K extends keyof T
? T[K] extends Translation
? K
: never
:
// This is where we extend this further to cover the dot separated case:
K extends `${infer TKey}.${infer Rest}`
? TKey extends keyof T
? T[TKey] extends undefined
? never
: T[TKey] extends Translations
? Rest extends DeepKeyToTranslation<T[TKey], Rest>
? K
: never
: never
: never
: never;
function printTranslationByDeepKey<T extends Translations, K extends string>(
t: T,
k: DeepKeyToTranslation<T, K>
) {
console.log(t[k]);
}
printTranslationByDeepKey(translations, "hello"); // Good!
printTranslationByDeepKey(translations, "hello.pl"); // Error
printTranslationByDeepKey(translations, "missing"); // Error
Det misslyckas eftersom:
Type 'T[TKey]' does not satisfy the constraint 'Translations'.
Type 'T[string]' is not assignable to type 'Translations'.
Type 'Translation | Translations | undefined' is not assignable to type 'Translations'.
Type 'undefined' is not assignable to type 'Translations'.()
Jag har hittat flera andra liknande frågor:
- Typescript: djup keyof av ett nästlat objekt
- Generisk funktion för att hämta ett värde i ett nästlat objekt
Den andra var till och med basen för min nuvarande implementering, men ingen av dem tillhandahåller en liknande funktion där du kan stoppa rekursion baserat på typen av värdet på vägen.
Svar #1
Jag är inte helt säker på att | undefined-delen av din Translations-typ behövs (kan ha fel här), och jag misstänker att det är det som orsakar det mesta av problemen.
Jag har gjort en liten omarbetning av ditt exempel och kom fram till en punkt som verkar fungera, ta en titt på det och se om det täcker ditt användningsfall!
type Language = "pl" | "en";
type Translation = { [lang in Language]: string };
type Translations = { [key: string]: Translation | Translations };
type DeepKeyToTranslation<T extends Translations, K extends string = string> = K extends keyof T
? T[K] extends Translation
? K
: never
: K extends `${infer TKey}.${infer Rest}`
? T[TKey] extends Translations
? DeepKeyToTranslation<T[TKey], Rest> extends never
? never
: K
: never
: never
declare function printTranslationByDeepKey<T extends Translations, K extends string>(
t: T,
k: DeepKeyToTranslation<T, K>
): K
const translations = {
hello: {
pl: "Dzieńdobry",
en: "Hello",
},
bool: {
yes: {
pl: "Tak",
en: "Yes",
},
no: {
pl: "Nie",
en: "No",
},
},
nested: {
deep: {
ok: {
pl: "Dzieńdobry",
en: "Hello",
}
}
}
};
printTranslationByDeepKey(translations, "hello") // ok
printTranslationByDeepKey(translations, "bool.yes") // ok
printTranslationByDeepKey(translations, "hello.pl") // err
printTranslationByDeepKey(translations, "missing") // err
printTranslationByDeepKey(translations, "nested.deep.ok") // ok
Lekplatslänk
Svar #2
Överväg denna lösning:
type Language = "pl" | "en";
const translations = {
hello: {
pl: "Dzieńdobry",
en: "Hello",
},
bool: {
yes: {
pl: "Tak",
en: "Yes",
},
no: {
pl: "Nie",
en: "No",
},
},
} as const;
type Translations = typeof translations;
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : keyof T extends Language ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type Result = KeysUnion<Translations>
function printTranslationByDeepKey<T extends Translations, K extends KeysUnion<Translations>>(
t: T,
k: K
) {}
printTranslationByDeepKey(translations, "hello"); // Good!
printTranslationByDeepKey(translations, "hello.pl"); // Error
printTranslationByDeepKey(translations, "missing"); // Error
Lekplats
KeysUnion - itererar rekursivt genom Translations-typen. Om Cache extends "" - betyder det att det är första iterationen och vi behöver anropa KeysUnion rekursivt med lämpliga argument men utan att använda Cache-memoisering, eftersom det inte är meningsfullt att ha en tom sträng i resultatet. Om Cache inte är en tom sträng, applicera Cache på resultatet (unionera det med resultatet) och anropa KeysUnion<T[P], ${Cache}.${P}> med nytt suffix.
T extends PropertyKey - om T inte längre är ett objekt - returnera Cache keyof T extends Language - om T är ett objekt med pl/en nycklar - ingen behöver tillämpas på resultatet - returnera Cache.
Du kan hitta en annan förklaring av denna nyttighetstyp med kommentarer här
Du kan hitta mycket mer förklaring på min blogg
Lista över relaterade svar: [ första, andra, tredje, fjärde ]
