user7499416
user7499416

Reputation:

Typing array of objects in TypeScript

Using typescript to type an array of objects:

Here are the interfaces:

export interface IUser {
  id: string;
  order: number;
}

export interface IUsersLogin {
  result: number;
  UserId: string;
}

export interface IUsersUsers {
  id: number;
  order: number;
}

export interface IOriginalData {
  Users?: IUser[];
  UsersLogins?: IUsersLogin[];
  UsersUsers?: IUsersUsers[];
}

Here I create an object using this interfaces:

const originalData: IOriginalData = {
  Users: [
    {
      id: "e4e2bb46-c210-4a47-9e84-f45c789fcec1",
      order: 1
    },
    {
      id: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae",
      order: 2
    }
  ],
  UsersLogins: [
    {
      result: 1,
      UserId: "e4e2bb46-c210-4a47-9e84-f45c789fcec1"
    },
    {
      result: 0,
      UserId: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae"
    }
  ],
  UsersUsers: [
    {
      id: 1,
      order: 0
    },
    {
      id: 2,
      order: 0
    }
  ]
};

And here I manipulate the data of this object to push it into another object:

interface IPushedDataItem {
  data: IUser | IUsersLogin | IUsersUsers;
}

type TypePushedData = Array<IPushedDataItem>;

let pushedData: TypePushedData = [];
Object.keys(originalData).forEach(item => {
  pushedData.push({
    data: originalData[item]
  });
});

In this process, I cant type pushedData properly, and it complains with data:originalData[item].

You can find this in the TypeScript playgound:

http://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgSQFUBnNOAbwCg5EATALjmJimQHMBuauaWtRpAFcAtgCM0XAL6VKoSLEQo0WXARJpiAGQhtkFblGDFBAGxgCR4qFxrqo+BkxbspMudHjJUGHHiKkoYjtifRoERyExCW5efjhIq1dZcA9FbxU-AHlWXSRMEwARTBhMULhggH5GfzQAbQBdG3KArR1kYiq1Fu1chqbgyuqBlr7KaUpsCCRmHhzkfKKS6uyEXIXi0oBeMuDGWu4aKhpjukYAImAAFmAAJlFRS4A2AFpsG4BGAAZny8xLgHZngBOYAADkuz3QlwArNh-qCgehcNh3mcADQHY6xKCMd6YyQYk5HE6nOBnURA6E3f6XbBA54AZloNxetOADOBoNEN2e-3+tFw0NEtH+mEwwHRmJo2MYN3x3HqhOaGh67T2mOJJ0MxjMuKVJzsDnOV1u9yerw+31+AOBYIhUNh8MRyNR+P1ZS1RlM5jgn3dtgCRrJFKpNLpjOZrNwHKBXJ5fIFwCFIrFEvlNEV3GGGnVRKlpPe-tmfBxvrdGvz4VlRZlZZO0gzYy4lC8yl8BAACoJiAALYC0RaYfCoYRlWgbIYBOAAHy6KraSBnc8CwSSMAAnmA8AAVTfALu9-uDuDbACCUCgmHXAB58Ae+wONsPgMIAHzNkzAeBgbsPweMXct3vI8NhPOBRkyUQACtgGwGAADoAGtgHXYgAApoFWeZCg2ABKeD0GgABRHAezQhARxPV8yh-Q9HxKeDaLIzU4HHJZZiwvIcJKWoKJfepuEkXCpGEyggA

Any help will be welcome!

Upvotes: 4

Views: 564

Answers (3)

georg
georg

Reputation: 214949

Let's simplify your example a bit:

export interface A { s: string }
export interface B { n: number }
export interface C { b: boolean }

export interface All {
    A: A;
    B: B;
    C: C;
}

//

let all: All = {
    A: {s: "s"},
    B: {n: 33},
    C: {b: true},
};

let other: Array<A | B | C> = [];

Now, this

Object.keys(all).forEach(k => other.push(all[k]))

will fail, because Object.keys is inferred as Array<string> and not Array<keyof All> as one might expect. See this comment as to why.

However, the same comment says that for k in obj infers k as keyof when using generics. We can use that to write our own strict version of Object.keys:

function strictKeys<T>(o: T): Array<keyof T> {
    let a = [];
    for (let k in o)
        a.push(k);
    return a;
}

and now

strictKeys(all).forEach(k => other.push(all[k]))

computes correctly!

Playground

Upvotes: 0

Eugene
Eugene

Reputation: 3957

  1. extend the definition of originalData to support indexer:
const originalData: IOriginalData & { [key: string]: (IUser | IUsersLogin | IUsersUsers)[] }
  1. you must iterate over array of items accessed by key when adding them to pushedData because it's not a single item:
Object.keys(originalData).forEach(item => {
  const subItems = originalData[item];
  if (subItems) {
    for (const subItem of originalData[item]) {
      pushedData.push({
        data: subItem
      });
    }
  }
});

the overall code is the following:

export interface IUser {
  id: string;
  order: number;
}

export interface IUsersLogin {
  result: number;
  UserId: string;
}

export interface IUsersUsers {
  id: number;
  order: number;
}

export interface IOriginalData {
  Users?: IUser[];
  UsersLogins?: IUsersLogin[];
  UsersUsers?: IUsersUsers[];
}

const originalData: IOriginalData & { [key: string]: (IUser | IUsersLogin | IUsersUsers)[] } = {
  Users: [
    {
      id: "e4e2bb46-c210-4a47-9e84-f45c789fcec1",
      order: 1
    },
    {
      id: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae",
      order: 2
    }
  ],
  UsersLogins: [
    {
      result: 1,
      UserId: "e4e2bb46-c210-4a47-9e84-f45c789fcec1"
    },
    {
      result: 0,
      UserId: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae"
    }
  ],
  UsersUsers: [
    {
      id: 1,
      order: 0
    },
    {
      id: 2,
      order: 0
    }
  ]
};

interface IPushedDataItem {
  data: IUser | IUsersLogin | IUsersUsers;
}

type TypePushedData = Array<IPushedDataItem>;

let pushedData: TypePushedData = [];
Object.keys(originalData).forEach(item => {
  const subItems = originalData[item];
  if (subItems) {
    for (const subItem of originalData[item]) {
      pushedData.push({
        data: subItem
      });
    }
  }
});

UPDATE

If you want to have a more strict check of string key, you can define it using in keyof syntax:

const originalData: IOriginalData & { [key in keyof IOriginalData]: (IUser | IUsersLogin | IUsersUsers)[] }

Though it will also require to cast the result of Object.keys(originalData) to (keyof IOriginalData)[].

The overall code will be the following:

export interface IUser {
  id: string;
  order: number;
}

export interface IUsersLogin {
  result: number;
  UserId: string;
}

export interface IUsersUsers {
  id: number;
  order: number;
}

export interface IOriginalData {
  Users?: IUser[];
  UsersLogins?: IUsersLogin[];
  UsersUsers?: IUsersUsers[];
}

export type OriginalDataUnion = IUser | IUsersLogin | IUsersUsers;

const originalData: IOriginalData & { [key in keyof IOriginalData]: OriginalDataUnion[] } = {
  Users: [
    {
      id: "e4e2bb46-c210-4a47-9e84-f45c789fcec1",
      order: 1
    },
    {
      id: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae",
      order: 2
    }
  ],
  UsersLogins: [
    {
      result: 1,
      UserId: "e4e2bb46-c210-4a47-9e84-f45c789fcec1"
    },
    {
      result: 0,
      UserId: "b95274c9-3d26-4ce3-98b2-77dce5bd7aae"
    }
  ],
  UsersUsers: [
    {
      id: 1,
      order: 0
    },
    {
      id: 2,
      order: 0
    }
  ]
};

interface IPushedDataItem {
  data: OriginalDataUnion;
}

type TypePushedData = Array<IPushedDataItem>;

let pushedData: TypePushedData = [];
for (const item of Object.keys(originalData) as (keyof IOriginalData)[]) {
  const subItems = originalData[item];
  if (subItems) {
    for (const subItem of subItems) {
      pushedData.push({
        data: subItem
      });
    }    
  }
}

Upvotes: 1

T.J. Crowder
T.J. Crowder

Reputation: 1074088

There are a couple of problems there.

originalData doesn't have an index signature. In general, because of TypeScript's static typing nature, highly-dynamic code (such as code going through all the properties of an object by name using Objct.keys) tends to be problematic because the static type information is lost, since the properties have different value types. If you can void doing that, which you can in your case, that's probably better.

Another issue is that you've given pushedData a type that expects an array of objects with a data property, but your code doesn't do that, it pushes the IUser etc. objects directly into the array instead of pushing objects where the data property is that IUser etc. object. I suspect you meant IPushedDataItem to be:

type IPushedDataItem  = IUser | IUsersLogin | IUsersUsers;

Assuming I'm correct about that, you can then create pushedData like this:

let pushedData: TypePushedData = [
    ...(originalData.Users ? originalData.Users : []),
    ...(originalData.UsersLogins ? originalData.UsersLogins : []),
    ...(originalData.UsersUsers ? originalData.UsersUsers : []),
];

Live on the playground

This also has the advantage of giving you a defined order to the entries in the resulting array (whereas with Object.keys, there is no guaranteed order per the specification, although all modern engines follow the same order for own properties as they would for getOwnPropertyKeys, it's just not guaranteed).

That would be less complicated if you made the properties non-optional (removed the ? in the properties in IOriginalData). Then it would be just:

let pushedData: TypePushedData = [
    ...originalData.Users,
    ...originalData.UsersLogins,
    ...originalData.UsersUsers,
];

If you really want objects with a data property instead (your original version of IPushedDataItem), then you'd use map:

let pushedData: TypePushedData = [
    ...(originalData.Users ? originalData.Users.map(data => ({data})) : []),
    ...(originalData.UsersLogins ? originalData.UsersLogins.map(data => ({data})) : []),
    ...(originalData.UsersUsers ? originalData.UsersUsers.map(data => ({data})) : []),
];

Live on the playground

...which again would be simpler if the properties weren't optional.

Upvotes: 0

Related Questions