Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: remapping object properties in typesafe

Tags:

typescript

What I want to do

I want to typing a javascript function below. This function remaps first argument's property names by second argument.

I use the remap function to create query string parameters. For example, from { param1: 1, param2: 2, param3: 3} to ?p1=1&p2=2&p3=3.

/**
 * @example
 *
 * const original = { a: 1, b: 'WOW', c: new Date(2019, 1, 1, 0, 0, 0) };  
 * const mapping = { a: 'hello', b: 'world', c: '!!!' };
 * 
 * > remap(original, mapping);
 * { hello: 1, world: 'WOW', '!!!': new Date(2019, 1, 1, 0, 0, 0) }
 */
const remap = (original, mapping) => {
  const remapped = {};
  Object.keys(original).forEach(k => {
    remapped[mapping[k]] = original[k];
  });
  return remapped;
};

My unsound code

I tried a code below, but this is unsound.

export const remap = <
  T extends { [key: string]: any },
  U extends { [P in keyof T]: string }
>(original: T, mapping: U) => {
  const remapped: any = {};

  Object.keys(original).forEach(k => {
    remapped[mapping[k]] = original[k];
  });

  // Problems
  // 1. remapped is declared as any, and cast required.
  // 2. All values are declared ad any.
  return remapped as { [P in keyof U]: any };
};

const remapped = remap(
  { a: 1, b: 'text', c: new Date() },
  { a: 'Hello', b: 'World', c: '!!!' }
);

console.info(remapped);
like image 749
edhiwo Avatar asked Oct 27 '25 08:10

edhiwo


1 Answers

You can type this correctly but it takes some conditional type magic:

// Converts object to tuples of [prop name,prop type]
// So { a: 'Hello', b: 'World', c: '!!!' }
// will be  [a, 'Hello'] | [b, 'World'] | [c, '!!!']
type TuplesFromObject<T> = {
    [P in keyof T]: [P, T[P]]
}[keyof T];
// Gets all property  keys of a specified value type
// So GetKeyByValue<{ a: 'Hello', b: 'World', c: '!!!' }, 'Hello'> = 'a'
type GetKeyByValue<T, V> = TuplesFromObject<T> extends infer TT ?
    TT extends [infer P, V] ? P : never : never;


export const remap = <
    T extends { [key: string]: any },
    V extends string, // needed to force string literal types for mapping values
    U extends { [P in keyof T]: V }
>(original: T, mapping: U) => {
    const remapped: any = {};

    Object.keys(original).forEach(k => {
        remapped[mapping[k]] = original[k];
    });
    return remapped as {
        // Take all the values in the map, 
        // so given { a: 'Hello', b: 'World', c: '!!!' }  U[keyof U] will produce 'Hello' | 'World' | '!!!'
        [P in U[keyof U]]: T[GetKeyByValue<U, P>] // Get the original type of the key in T by using GetKeyByValue to get to the original key
    };
};

const remapped = remap(
    { a: 1, b: 'text', c: new Date() },
    { a: 'Hello', b: 'World', c: '!!!' }
);
// const remapped: {
//     Hello: number;
//     World: string;
//     "!!!": Date;
// }
like image 155
Titian Cernicova-Dragomir Avatar answered Oct 28 '25 21:10

Titian Cernicova-Dragomir



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!