[Typescript] Dynamic types: Use TypeScript's Mapped Types and Template Literal Types Together
we're going to dive deep into a more complex example in which we combine mapped types, key remapping, template literal types, and indexed access types to statically type a highly dynamic JavaScript function in TypeScript.
Start with following code:
function createGetterObject(obj: any): any { const newObj: any = {}; for (const key of Object.keys(obj)) { const cpK = key[0].toUpperCase() + key.substr(1); const getterKey = `get${cpK}`; newObj[getterKey] = () => obj[key]; } return newObj; } const user = createGetterObject({ name: "Wan", twitter: "zhentiw" }) console.log(user) console.log(user.getName()) console.log(user.getTwitter())
We want to get better typings support.
function createGetterObject<T>(obj: T): PropGetters<T> { const newObj: any = {}; for (const key of Object.keys(obj)) { const cpK = key[0].toUpperCase() + key.substr(1); const getterKey = `get${cpK}`; newObj[getterKey] = () => obj[key]; } return newObj; } type PropGetters<T> = { }
Get an error:
This is because `T` can be any as well. We need to limit T type by telling that `T` can be only for Object:
function createGetterObject<T extends Record<string, any>>(obj: T): PropGetters<T> {
In `PropGetter`, we want to create similar to
type PropGetters<T> = { getName: () => sting getTwitter: () => string }
How to make those?
We can start from:
type PropGetters<T> = { [Key in keyof T]: () => T[Key] }
keyof T: get all the keys of T, so we got `name` & `twitter`
T[Key]: as lookup type, `name` prop result in string `Wan`.
This is what we get as a result:
`name` & `twitter` are functions type which return string, not really what we want.
Template Literal Types
type PropGetters<T> = { [Key in keyof T as `get${Key}`]: () => T[Key] }
Got error:
This is because Key can be string, number, boolean... , what we want is just string type, so we can do:
type PropGetters<T> = { [Key in string & keyof T as `get${Key}`]: () => T[Key] }
Now, we got:
We actual want `getName` not as `getname`, to fix this, we can do:
type PropGetters<T> = { [Key in string & keyof T as `get${Capitalize<Key>}`]: () => T[Key] }
Now the type should works. If we add another prop `id`:
Typescript can tell the return value is a number type.
Final piece to limit `T` in `PropGetters`:
type PropGetters<T extends Record<string, any>> = { [Key in string & keyof T as `get${Capitalize<Key>}`]: () => T[Key] }