I'm trying to write a TypeScript function that takes an input object and returns a result object with properties derived from the input.
This is what I have so far: (this function should just create a new object with renamed keys)
type t1 = {
a: number,
b: number,
c: number
}
type t2 = {
i: number,
j: number,
k: number
}
function translate(input: Partial<t1>): Partial<t2> {
const result: Partial<t2> = {};
if("a" in input) result["i"] = input["a"];
if("b" in input) result["j"] = input["b"];
if("c" in input) result["k"] = input["c"];
return result;
}
const result = translate({
a: 1, b: 2
});
console.log(result); // logs { i: 1, j: 2 }
this works, but I want my result to be strongly typed based on the input object. For example, if the input object is { a: 1 }, the function should return an object of type { i: number }. If the input object is { a: 1, b: 2 }, the function should return an object of type { i: number, j: number } etc.
Update:
This is what I have now based on @user137794 's answer:
type t1 = {
a: number,
b: number,
c: number
}
type t2 = {
i: number,
j: number,
k: number
}
/** this defines how field names are translated */
const renameMap = { a: 'i', b: 'j', c: 'k' } as const;
type RenameKeys<T, KeyMap extends Record<string, string>> = {
[K in keyof T as K extends keyof KeyMap ? KeyMap[K] : never ]: T[K]
}
function translate<T extends Partial<t1>> (input: T): RenameKeys<T, typeof renameMap> {
const result: Partial<t2> = {};
for(const key of Object.keys(renameMap) as (keyof typeof renameMap)[])
if (key in input) result[renameMap[key]] = input[key];
return result as RenameKeys<T, typeof renameMap>;
}
const result = translate({
a: 1, b: 2
});
console.log(result); // logs { i: 1, j: 2 }
This function returns the a strongly typed object based on the input. It only works for simple 1:1 key renaming however. What should I do for more complex functions, where multiple fields from input object are mapped to a single output field?
It would be ideal if I could define the maping logic something like this:
const translationMap = {
i: (sourceObject: t1) => sourceObject.a,
j: (sourceObject: t1) => sourceObject.a + sourceObject.b,
k: (sourceObject: t1) => sourceObject.b || sourceObject.c,
}
and have TypeScript infer the type of each property in the result automatically. If that's not possible, any other workarounds are welcome
To answer my own question:
This function translates an object into another object and returns a strongly typed result. It works well with complex translation logic
type ComputedMap<SRC> = {
[ATTR: string]: (obj: SRC) => unknown;
};
type MappedReturnType<SRC, CM extends ComputedMap<SRC>> = { [ATTR in keyof CM]: ReturnType<CM[ATTR]> };
/**
*
* @param src source object
* @param computedMap key-function map describing how properties should translate. Functions Take the source object as an argument
* @returns
*/
function transformObject<SRC extends object, CM extends ComputedMap<SRC>>(
src: SRC,
computedMap: CM
): MappedReturnType<SRC, CM> {
const result: Partial<Record<string, unknown>> = {};
for (const key in computedMap) {
const transformer = <(obj: SRC) => unknown>computedMap[<string>key];
result[key] = transformer(src);
}
return result as MappedReturnType<SRC, CM>;
}
type t1 = {
a?: number,
b?: number,
c?: number,
}
const src: t1 = {
a: 1,
b: 2,
c: 3,
}
const r = transformObject(src, {
i: (o) => o.a,
j: (o) => o.a && o.b && (o.a + o.b),
k: (o) => !(o.b || o.c),
a: (o) => o.c,
});
console.log(r); // logs { "i": 1, "j": 3, "k": false, "a": 3 }
(based on this answer by @Matt McCutchen)