How to write a TypeScript function that maps (translates) an object into another object and infers the return type?

78 Views Asked by At

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

2

There are 2 best solutions below

0
On BEST ANSWER

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)

3
On

You can rename the keys of a type using

type RenameKeys<T, KeyMap extends Record<string, string>> = {
  [K in keyof T as K extends keyof KeyMap ? KeyMap[K] : K]: T[K]
}

and use it like

function translate<T> (input: T): RenameKeys<T, { a: 'i', b: 'j', c: 'k' }> {
  /* ... */
}