Home > other >  TypeScript: Creating new object type from array of objects
TypeScript: Creating new object type from array of objects

Time:08-31

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 }
]);

CodePudding user response:

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>>;
  }
  • Related