Dynamic dispatch with single repository

19 hours ago 4
ARTICLE AD BOX

The only supported approach to dealing with correlated union types as described in microsoft/TypeScript#30581 is as described in microsoft/TypeScript#47109. As you see, it involves rewriting your types so that you are explicitly indexing into a mapped type with a generic index.

That approach requires that TypeScript can "see" that your generic indexing operation at the value level corresponds to an analogous generic indexing operation at the type level. The type of verbs must explicitly be something like the mapped type {[P in keyof ParamsByType]: Verb<P>}, so that indexing into it with action.verbType of generic type K extends keyof ParamsByType will result in the generic type Verb<K>.

If the type of verbs is merely structurally equivalent to that mapped type, the approach will stop working. So a declaration like const verbs = {⋯} that results in const verbs: {goTo: Verb<number>}; attack: Verb<string>} isn't going to work. TypeScript can't reason in the abstract about that type. It cannot "notice" that the type could be rewritten as {[P in keyof ParamsByType]: Verb<P>}. So when you do the indexing, it just falls back to the non-generic version where you end up with a union like Verb<number> | Verb<string> and the correlation is broken.


The simplest way to fix it is to do it the "scattered" way you did it, where you first define your VerbMap type and then define verbs in terms of it.

But if you want to define your mapping object first and derive ParamsByType from it, that's fine. You just need to then go on to also derive the mapped type from it. You'll start with your mapping object, and then you'll end up needing to assign that object to a new type. This is easiest to do with a new variable as well. For example:

const _verbs = { goTo, attack } as const; // <-- renamed out of the way type VerbMap = typeof _verbs; // unchanged type ParamsByType = { [K in keyof VerbMap]: Parameters<VerbMap[K]["execute"]>[0]; }; // unchanged const verbs: { [K in keyof VerbMap]: Verb<ParamsByType[K]> } = _verbs; // new type

We've renamed verbs out of the way to _verbs, so that we can later use the name verbs for the object whose type is more useful to TypeScript. Yes, that looks like nothing happened: the type of _verbs and the type of verbs will tend to display the same. But again, the difference of internal representation is important. The type of _verbs has, as far as TypeScript sees, no dependency on ParamsByType (any dependency it can see is the other way around). But the type of verbs is explicitly the mapped type you need to index into.

It would be wonderful if TypeScript would notice these correlations without having to curate representations of types for it. But it doesn't. So these are the hoops you need to jump through to get it working.

Playground link to code

Read Entire Article