Home > Software design >  Map array of objects to object type in function params
Map array of objects to object type in function params

Time:02-03

I have this function definition:

function example<
  O extends { property: P; required: boolean },
  P extends string
>(
  arr: O[]
): {
  [P in O["property"]]: O["required"] extends true
    ? string
    : string | undefined;
};

example([
  { property: "hey", required: true },
  { property: "ho", required: false },
]);

Which gives this typing:

function example<{
    property: "hey";
    required: true;
} | {
    property: "ho";
    required: false;
}, string>(arr: ({
    property: "hey";
    required: true;
} | {
    property: "ho";
    required: false;
})[]): {
    hey: string | undefined;
    ho: string | undefined;
}

required: true should mean that the returned object definitely has the associated property, and required: false should mean it may or may not have it, i.e. string | undefined.

So hey should just be string in this scenario, as required is true.

If required is true for both of them, it types them both correctly as just string, but if one is false then it seems to widen the type for every key/value.

Is it possible to map types individually this way?

Playground example.

CodePudding user response:

The problem with

{
  [P in O["property"]]: O["required"] extends true
    ? string
    : string | undefined;
}

is that O will likely be a union, so O["property"] and O["required"] will be separate uncorrelated unions. If O is { property: "hey"; required: true } | { property: "ho"; required: false }, then O["property"] is "hey" | "ho" and O["required"] is true | false, and any association between pieces of each union has been lost.

Another way to look at the problem is that the property value type of your mapped type does not mention the key type parameter P at all, so the output cannot possibly have property value types which depend on individual keys.


One way to fix this is to continue to iterate P over O["property"] but then filter O to the proper member depending on P before getting the required property from it. We can use the Extract<T, U> utility type to filter unions this way:

{
  [P in O["property"]]: Extract<O, { property: P }>["required"] extends true
  ? string
  : string | undefined;
};

That results in

const result = example([
  { property: "hey", required: true },
  { property: "ho", required: false },
]);
/* const result: {
  hey: string;
  ho: string | undefined;
} */

as desired.


Another way to fix this is to make use of key remapping in mapped types, which lets you iterate the type parameter over any union whatsoever, and then change the key to be a function of each member of the union. Like this:

{
  [T in O as T["property"]]: T["required"] extends true
  ? string
  : string | undefined;
};

So instead of iterating P over each property and then having to find the right required, we iterate T over each piece of O and then just index into T to get property and required. That results in the same output:

const result = example([
  { property: "hey", required: true },
  { property: "ho", required: false },
]);
/* const result: {
  hey: string;
  ho: string | undefined;
} */

Playground link to code

  • Related