4

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);
2
  • 1
    This looks like an X/Y problem. What do you want to use the remap function for? Commented Apr 1, 2019 at 11:41
  • Thank you for your replying Sefe. I added a purpose of remap function. Commented Apr 1, 2019 at 12:00

1 Answer 1

6

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;
// }
Sign up to request clarification or add additional context in comments.

10 Comments

@PrzemyslawPietrzak 10x :). I added some comments explaining the magic a bit :)
The most recent additions to TS make it possible to write code that makes my head spin. Using typings like this is pretty cool to make things work, but in production code you'll be in maintenace hell very quickly. I am using conditional typing in my code but I try to keep it simple; otherwise I spend too much time to wrap my head around the typings of my own code.
@Sefe I just provide the gun, what people do with it is their own doing :P. You are right complex conditional types are hard to understand. It's best to keep them simple, or at least their behavior simple to understand.. That being said I tend to prefer having my code as type safe as possible so I would probably use the typed remap above
In these cases I try to find alternatives that are easier to type. In a .d.ts file I would use fancy typing like that to annotate third-party code that has been created with only JS in mind (which is always fair game IMHO).
@GârleanuAlexandru-Ștefan I use that trick to enable distributive conditional types. The Infer TT is to get a type parameter I can then distribute over (distribution only happens over naked type parameters)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.