Reputation: 57
I'm having a problem achieving proper validation given the types below:
type T1 {
__typename: "T1";
action: "ACTION_1" | "ACTION_2";
user: string;
}
type T2 {
__typename: "T2";
action: "ACTION_3" | "ACTION_4" | "ACTION_5";
}
type T3 {
__typename: "T3";
action: "ACTION_6";
note: string;
}
type T = T1 | T2 | T3
Based on the above, I'm trying to achieve a type that would validate object below:
const obj: MappedObj = {
T1: {
ACTION_1: "some string",
ACTION_2: "some string"
},
T2: {
ACTION_3: "some string",
ACTION_4: "some string",
ACTION_5: "some string",
},
T3: {
ACTION_6: "some string"
}
}
Basically, I tried to create an indexed type, iterating through __typename
of T
, and then in that property to have another iteration of action
. Ideally, I would like the action
to be based on the "current" __typename
but my current solution does not distinguish from different variants of T
and just requires ALL possible actions to be specified in a object:
type MappedObj = { [key in T["__typename"]: { key2 in T["action"]: string } }
For each key in T["typename"]
I get an error in this fashion:
Type '{ ACTION_1: string; ACTION_2: string; }' is missing the following properties from type '{ ACTION_3: string; ACTION_4: string; ACTION_5: string; ACTION_6: string; ts(2739)
Is there a way to tie action
to __typename
of "current" property?
Upvotes: 2
Views: 894
Reputation: 328097
With key remapping in mapped types as introduced in TypeScript 4.1, you are allowed to iterate over arbitrary union types, not just key-like types, as long as you re-map the members of that union to keys. That means your MappedObj
could be defined like this:
type MappedObj = {
[U in T as U["__typename"]]: {
[P in U["action"]]: string
}
};
We are iterating over the union T
. For each member U
of that union, we make a property whose key is U["__typename"]
and whose value is the equivalent of Record<U["action"], string>
. That produces the type you're looking for:
/* type MappedObj = {
T1: {
ACTION_1: string;
ACTION_2: string;
};
T2: {
ACTION_3: string;
ACTION_4: string;
ACTION_5: string;
};
T3: {
ACTION_6: string;
};
} */
Note that this is also possible without key remapping (and therefore in TypeScript versions earlier than 4.1), but because you're iterating over the union T["__typename"]
, for each such key K
you need to extract from T
the member which is assignable to {__typename: K}
. So it could be written like this:
type MappedObj = {
[K in T["__typename"]]: {
[P in Extract<T, { __typename: K }>["action"]]: string
}
};
using the Extract<X, Y>
utility type which evaluates to the member(s) of the union X
which are assignable to Y
. That's a bit more work than before: remapping can just iterate U in T
, and without it you have to synthesize U
from K in T["__typename"]
. I usually suggest key remapping nowadays, but it can still be useful to know how to do it a different way.
Upvotes: 1