I'm trying to type a method which remaps object's keys by a given map. For example:
> remapKeys({key1: "xxx", key2: "yyy"}, {key1: "newKey"})
{ newKey: 'xxx', key2: 'yyy' }
However, I'm running into an issue when I try to make this method return a strongly-typed object. My current attempts looks like this:
type MappedKeys<T extends { [key: string]: any }, M extends { [K in keyof Partial<T>]: string }> =
| Omit<T, keyof M>
| { [P in keyof T & keyof M as M[P]]: T[P] };
function remapKeys<T extends { [key: string]: any }, M extends { [K in keyof Partial<T>]: string }>(
obj: T,
mappings: M,
): MappedKeys<T, M>;
The idea here is that we try to make a union of two types:
- subset of original properties that are not specified in
mappings
object - new type created from the intersection of
obj
andmappings
properties
This doesn't work, however, because for some reason the implicit type of the returned value is an object with keys of type string
, not 'newKey' | 'key2'
.
Link to TypeScript playground.
CodePudding user response:
You need an intersection (&
), not a union (|
):
type MappedKeys<T extends { [key: string]: any }, M extends { [K in keyof Partial<T>]: string }> =
(Omit<T, keyof M> & { [P in keyof T & keyof M as M[P]]: T[P] }) extends infer O ? { [K in keyof O]: O[K] } : never;
Also, for extra flair, I have added extends infer O ? ...
to simplify the type so you don't get the ugly Omit<...> & { ... }
in tooltips. It'll always be a simple object now ({ ... }
).
Here's the function now:
declare function remapKeys<T extends { [key: string]: any }, M extends { [K in keyof Partial<T>]: string }>(
obj: Narrow<T>,
mappings: Narrow<M>,
): MappedKeys<T, M>;
I've used this Narrow
utility type so we don't need to use as const
:
type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });
Otherwise, the second parameter will get inferred as { key1: string }
and then we can't remap keys.