Home > Back-end >  How to get the type from array of objects in TypeScript
How to get the type from array of objects in TypeScript

Time:11-18

In TypeScript, I have arrays of objects like this

const props1 = [
  { propName: 'id', propValue: 1 },
  { propName: 'name', propValue: 'John' },
  { propName: 'age', propValue: 30 }
];

const props2 = [
  { propName: 'id', propValue: 2 },
  { propName: 'role', propValue: 'admin' }
];

const props3 = [
  { propName: 'job', propValue: 'developer' },
  { propName: 'salary', propValue: 1000 }
];

I want to create a function createObject that I can pass an array of them to it, and it should return an object that its keys are propName with values propValue

This is the desirable results type

const obj1 = createObject(props1); // Returned type should be: { id: number; name: string; age: number }
const obj2 = createObject(props2); // Returned type should be: { id: number; role: string }
const obj3 = createObject(props3); // Returned type should be: { job: string; salary: number }

This is my try, but doesn't return the correct type

function createObject<T extends { propName: string; propValue: any }[]>(propertiesObject: T) {
  const obj: { [key: string]: any } = {};

  for (const { propName, propValue } of propertiesObject) {
    obj[propName] = propValue;
  }

  return obj;
}

CodePudding user response:

The first thing we need to do is change the arrays. Currently, each propName is only of type string and there is no way to extract the original literal type.

We need to initialize them with as const.

const props1 = [
  { propName: 'id', propValue: 1 },
  { propName: 'name', propValue: 'John' },
  { propName: 'age', propValue: 30 }
] as const

const props2 = [
  { propName: 'id', propValue: 2 },
  { propName: 'role', propValue: 'admin' }
] as const

const props3 = [
  { propName: 'job', propValue: 'developer' },
  { propName: 'salary', propValue: 1000 }
] as const

Now the actual function.

type ToPrimitive<T> =
  T extends string ? string
  : T extends number ? number
  : T extends boolean ? boolean
  : T;

function createObject<
  T extends readonly { propName: string; propValue: any }[]
>(propertiesObject: T): { 
  [K in T[number] as K["propName"]]: ToPrimitive<K["propValue"]> 
} {
  const obj: { [key: string]: any } = {};

  for (const { propName, propValue } of propertiesObject) {
    obj[propName] = propValue;
  }

  return obj as { [K in T[number] as K["propName"]]: ToPrimitive<K["propValue"]> };
}

We also need to add readonly to the constraint of T to allow our as const arrays. The return type is a simple mapped type which maps over the elements K of T[number] and uses K["propName"] as the keys.

K["propValue"] currently also holds the narrowed literal types. But we actually want to widen them here back to their primitive types. That's where ToPrimitive comes into play.

The return type is now quite complex. TypeScript is not able to correlate it to the implementation anymore. That's why the type assertion at the end is needed.


Playground

CodePudding user response:

When you return the object at the end, you can use a mapped type to create a key in an object type for each T[number] in the argument. ['propName'] can be used to extract the key on the left-hand side, and ['propValue']` will extract the value.

const props1 = [
  { propName: 'id', propValue: 1 },
  { propName: 'name', propValue: 'John' },
  { propName: 'age', propValue: 30 }
] as const;
function createObject<T extends readonly { propName: string; propValue: unknown }[]>(propertiesObject: T) {
  const obj: { [key: string]: unknown } = {};

  for (const { propName, propValue } of propertiesObject) {
    obj[propName] = propValue;
  }

  return obj as {
    [K in T[number] as K['propName']]: K['propValue']
  };
}

const obj1 = createObject(props1); // Returned type should be: { id: number; name: string; age: number }

If

const obj1: {
    id: 1;
    name: "John";
    age: 30;
}

is too specific, and you want just number and string in the values instead, you could either change the typing of the props1, eg

const props1 = [
  { propName: 'id', propValue: 1 as number },
  { propName: 'name', propValue: 'John' as string },
  { propName: 'age', propValue: 30 as number }
] as const;

Or create a utility type that widens strings to string and numbers to number.

type Widen<T extends unknown> =
    T extends string ? string
  : T extends number ? number
  // etc
  : T;
  return obj as {
    [K in T[number] as K['propName']]: Widen<K['propValue']>
  };
  • Related