1

Is there a way to infer the name value from an array of objects and have the new object use those values as keys for the output type?


interface TypeA {
  name: string;
  value: number;
}

interface TypeB {
  [key: string]: { value: any };
}

// can this be created without hard-coding a new type containing the values?
interface OutputType {
  test: {value: any},
  test2: {value: any},
}

const arrayOfObjectsToObject = (array: TypeA[]):OutputType =>
  array.reduce((acc: TypeB, { name, value }: TypeA) => {
    acc[name] = { value };
    return acc;
  }, {});

const result = arrayOfObjectsToObject([ // {test:{value:1}, test2:{value:2}} etc...
  { name: 'test', value: 1 },
  { name: 'test2', value: 2 }
]);

1 Answer 1

1

We could define a generic type ToOutputType which takes a tuple and transforms the type to the desired object type using a mapped type.

type ToOutputType<T extends { name: string, value: any }[]> = {
    [K in T[number] as K["name"]]: { value: K["value"] }
} 

We also modify the arrayOfObjectsToObject to make it generic.

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

const arrayOfObjectsToObject = 
  <
    T extends { name: K, value: any }[], 
    K extends string
  >(array: readonly [...T]): Expand<ToOutputType<T>> => {
    return array.reduce((acc, { name, value }) => {
      acc[name] = { value };
      return acc;
    }, {} as any) as Expand<ToOutputType<T>>;
  }

T will hold the tuple passed to the function. K will be used to narrow the strings in the tuple to literal types. We use the type Expand<ToOutputType<T>> as the return type. The Expand type is used just to make the type prettier.


When you call the function, you get the following result.

const result = arrayOfObjectsToObject([
  { name: 'test', value: 1 },
  { name: 'test2', value: 2 }
]);

const a = result.test
//    ^? { value: number; }

const b = result.test2
//    ^? { value: number; }

Notice that the type of value is number in both cases. TypeScript automatically widens the numbers to number. To prevent this, we can use as const.

const result = arrayOfObjectsToObject([
  { name: 'test', value: 1 },
  { name: 'test2', value: 2 }
] as const);

const a = result.test
//    ^? { value: 1; }

const b = result.test2
//    ^? { value: 2; }

Playground


If you don't want to use as const, we can also use an extra magic generic type for inference.

type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};

const arrayOfObjectsToObject = 
  <
    T extends { name: K, value: N }[], 
    N extends { [k: string]: N | T | [] } | Narrowable,
    K extends string
  >(array: readonly [...T]): Expand<ToOutputType<T>> => {
    return array.reduce((acc, { name, value }) => {
      acc[name] = { value };
      return acc;
    }, {} as any) as Expand<ToOutputType<T>>;
  }
Sign up to request clarification or add additional context in comments.

1 Comment

I think you forgot to change the types of a and b after using as const.

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.