2

I have two objects, and my custom map function to map props of one object into values of another, e.g:

const obj1 = {
  host: 'clientNameHost',
  pass: 'clientNamePass',
};

const obj2 = {
  clientNamePass: '12345',
  clientNameHost: 'http://localhost:3000',
};

const mapParams = <T, K>(source: T, params: K) => Object.entries(params)
.reduce((acc, [param, sourceProp]) => ({ ...acc, [param]: source[sourceProp] }), {});

mapParams(obj2, obj1) //{host: "http://localhost:3000", pass: "12345"}

I'm having a hard time, trying to type this function. Is there some way to type properly mapParams function?

1
  • You can write sourceProp as keyof T like so: const mapParams = <T, K>(source: T, params: K) => Object.entries(params).reduce((acc, [param, sourceProp]) => ({ ...acc, [param]: source[sourceProp as keyof T] }), {}); Commented Feb 19, 2020 at 8:25

2 Answers 2

3

The solution is to narrow the generic types in order to show the relation between keys and values in both arguments:

const obj1 = {
    host: 'clientNameHost',
    pass: 'clientNamePass',
  } as const;

  const obj2 = {
    clientNamePass: '12345',
    clientNameHost: 'http://localhost:3000',
  } as const;

  const mapParams = <T extends Record<string, any>, K extends keyof T, P extends Record<T[K], any>>(source: T, params: P) => Object.entries(params)
  .reduce((acc, [param, value]) => ({ ...acc, [param]: source[value as T[K]] }), {});

  console.log(mapParams(obj1, obj2)) //{host: "http://localhost:3000", pass: "12345"}

Key parts:

  • T extends Record<string, any> first type is a Record with string keys
  • K extends keyof T respresents keys of the T
  • P extends Record<T[K], any> crucial part - we want object which will have keys equal values in T
  • as const was added in order to properly infer the narrowed types

Above types create a bound between first argument values and second argument keys.

PS. sorry for reverting arguments order.

Sign up to request clarification or add additional context in comments.

Comments

0

When typing functions what I care about the most is argument and return types.

You could type the arguments like this:

<InputProps, TargetProps extends Record<keyof TargetProps, keyof InputProps>>(
  source: InputProps,
  params: TargetProps
) => ...

Typescript will complain if the second object doesn't match the first one.

For the return value you can define a utility type as follows:

type MapFrom<A, B> = Record<keyof A, B[keyof B]>;

By using the Object.proptotype.entries type argument we avoid having to cast sourceProp.

To keep the function signature we need to assert the type of initialValue in the reduce.

I also use const assertions to give object literals readonly properties

This is the complete result:

type MapFrom<A, B> = Record<keyof A, B[keyof B]>;

const obj1 = {
  host: "clientNameHost",
  pass: "clientNamePass"
} as const;

const obj2 = {
  clientNamePass: "12345",
  clientNameHost: "http://localhost:3000"
} as const;

const mapParams = <InputProps, TargetProps extends Record<keyof TargetProps, keyof InputProps>>(
  source: InputProps,
  params: TargetProps
) =>
  Object.entries<keyof InputProps>(params).reduce(
    (acc, [param, sourceProp]) => ({
      ...acc,
      [param]: source[sourceProp]
    }),
    {} as MapFrom<TargetProps, InputProps>
  );

const obj3 = mapParams(obj2, obj1);

Here you can try it out.

Comments

Your Answer

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