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?
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;
} */