Enhanced Type Safety in TypeScript 4.1
The latest version of TypeScript, TS 4.1, introduces support for typed string-key lookups and interpolation using template literal types.
With this new feature, we can now access dictionary keys or object paths deeply by providing a string argument:
t("footer"); // ✅ { copyright: "Some copyrights"; }
t("footer.copyright"); // ✅ "Some copyrights"
t("footer.logo"); // ❌ should trigger compile error
Let's delve into how we can define a suitable return type for the translate function t
, how to emit compile errors for non-matching key arguments, provide IntelliSense support, and explore an example of string interpolation.
1. Key Lookup: Return Type
// Returns the property value from object O given the property path T, otherwise 'never'
type GetDictValue<T extends string, O> =
T extends `${infer A}.${infer B}` ?
A extends keyof O ? GetDictValue<B, O[A]> : never
: T extends keyof O ? O[T] : never
function t<P extends string>(p: P): GetDictValue<P, typeof dict> { /* implementation */ }
Check out the Playground to play with the code: Playground
2. Key Lookup: IntelliSense and Compile Errors
To enforce type safety and provide IntelliSense suggestions, we use conditional types like so:
// Returns the same string literal T if props match, else 'never'
type CheckDictString<T extends string, O> =
T extends `${infer A}.${infer B}` ?
A extends keyof O ? `${A}.${Extract<CheckDictString<B, O[A]>, string>}` : never
: T extends keyof O ? T : never
function t<P extends string>(p: CheckDictString<P, typeof dict>): GetDictValue<P, typeof dict> { /* implementation */ }
Explore the Playground for interactive examples: Playground
3. Interpolation
Incorporating string interpolation into translations:
// Helper types for string interpolation
type Keys<S extends string> = S extends '' ? [] :
S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : never
type Interpolate<S extends string, I extends Record<Keys<S>[number], string>> =
S extends '' ? '' :
S extends `${infer A}{{${infer B}}}${infer C}` ?
`${A}${I[Extract<B, keyof I>]}${Interpolate<C, I>}`
: never
Example:
type Dict = { "key": "yeah, {{what}} is {{how}}" }
type KeysDict = Keys<typeof dict["key"]> // ['what', 'how']
type I1 = Interpolate<typeof dict["key"], { what: 'i18next', how: 'great' }>;;
// Result: "yeah, i18next is great"
function t<K extends keyof Dict, I extends Record<Keys<Dict[K]>[number], string>>(k: K, args: I): Interpolate<Dict[K], I> { /* implementation */ }
const ret = t('key', { what: 'i18next', how: 'great' } as const);
// Output: "yeah, i18next is great"
Try it out on the Playground: Playground
For more details, refer to the discussion on deep keyof of a nested object: Typescript: deep keyof of a nested object
Note: These advanced typing features may have limits due to complexity and recursion depth restrictions in TypeScript compiler.