I want to strongly type a dot separated path to a specific type. Let's say we have a recursive structure where every leaf is a specific type, in this case translations by a set of languages:
type Language = "pl" | "en";
type Translation = { [ lang in Language ]: string };
type Translations = { [key: string]: Translation | Translations | undefined };
An example of this might be:
const translations = {
hello: {
pl: "Dzieńdobry",
en: "Hello",
},
bool: {
yes: {
pl: "Tak",
en: "Yes",
},
no: {
pl: "Nie",
en: "No",
},
},
};
I want a type for the paths: "hello"
, "bool.yes"
and "bool.no"
, but not to "bool"
or "missing"
or "bool.foo" or
"hello.pl"`. Here's what I have so far:
For a single layered translations file:
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.
So we need some template strings and infer
s. Unfortunately I can't seem to get this working. It seems to forget that I've already asserted that 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
It fails because:
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'.()
I've found several other similar questions:
The second was even the base for my current implementation, but neither provide a similar function where you can stop recursion based on the type of the value at the path.