1

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.

2
  • 1
    Do these approaches meet your needs? If so I could write up an answer; if not, what am I missing? Commented Feb 2, 2023 at 16:15
  • That's spot on, thanks @jcalz Commented Feb 2, 2023 at 16:18

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

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.