TypeScript: Contextual type can't be provided to a mapped type intersected with an object type
Bug Report
๐ Search Terms
intersection, mapped type, contextual type
๐ Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ
โฏ Playground Link
๐ป Code
type Action<TEvent extends { type: string }> = (ev: TEvent) => void;
interface MachineConfig<TEvent extends { type: string }> {
schema: {
events: TEvent;
};
on?: {
[K in TEvent["type"]]?: Action<TEvent extends { type: K } ? TEvent : never>;
} & {
"*"?: Action<TEvent>;
};
}
declare function createMachine<TEvent extends { type: string }>(
config: MachineConfig<TEvent>
): void;
createMachine({
schema: {
events: {} as { type: "FOO" } | { type: "BAR" },
},
on: {
FOO: (ev) => {
ev.type; // should be 'FOO', but `ev` is typed implicitly as `any`
},
},
});
๐ Actual behavior
An implicit any pop-ups when the contextual type could be, somewhat easily, provided.
๐ Expected behavior
This should just work ๐ I know a workaround for this issue - the workaround is to use a single mapped type instead of an intersection and just โdispatchโ to the correct value in the template~ part of the mapped type, like here. However, this is way less ergonomic than an intersection AND the mapped type is no longer homomorphic which could matter for some cases (well, the original mapped type here is not homomorphic either, but it could be)
I already have a draft PR open to fix this issue, here. I only need some help with the stuff mentioned in the comment here
About this issue
- Original URL
- State: open
- Created 2 years ago
- Comments: 16 (7 by maintainers)
Lemme try an explanation ๐
Weโre letting the user provide an object in the form
now, we want the return type of
actionPayloadto be the same assomethingup inPayloadAction. But we cannot just add a generic<T>somewhere to make sure thatโs the case - because this object is not the only object being passed in, but one of many objects inside a config object:Now, TS has no syntax to allow for different ActionPayload types for
fooandbarwhile having those internally consistent (fooonly hasFooActionPayloadboth onreducerandprepareandbaronly hasBarActionPayloadboth onreducerandprepare).So what we do is that we let TypeScript infer this whole configuration object including all reducers (the โfirst passโ) and then, when we have that, we use that
ConfigObjectto restrict it against itself (the โsecond passโ) -ConfigObject extends Validated<ConfigObject>whereValidatedis a generic that infersFooActionPayloadfromReturnType<ConfigObject['reducers']['foo']['prepare']>and makes sure that the second argument toreducermatches that type.Itโs amazing that we could do something like that in the first place, but itโs also pretty necessary here to make the api work in a type-safe manner.
Good point, here is a test case: Playground link