Aaron Bloom
Aaron Bloom

Reputation: 23

Stricter union types when using mapped types in TypeScript

I'm attempting to use a mapped type to provide more type safety when using union types in a map. There does not seem to be a way to provide type safety between the key/value when using a property type (e.g. ['value']) as the type for the key (K).

I would like to avoid creating a unique model by hand to allow for this.

Code:

interface IAction { value: string; }

type ActionMapper<A extends IAction> = {
   [K in A['value']]: A;
}

interface IActionOne { value: 'action_one' }

interface IActionTwo { value: 'action_two' }

type Actions = IActionOne | IActionTwo;

const reducerMap: ActionMapper<Actions> = {
  action_one: { value: 'action_one' },
  action_two: { value: 'action_one' }, // expecting this line to fail
}

I have commented the line I expected to fail.

I feel I should be able to leverage the key (K in) to provide the correct type as the value. However, I currently use A, which provides the IAction implementation where the value is of type string - I want to avoid this.

Is this possible in the current version of TypeScript?

Upvotes: 1

Views: 588

Answers (1)

jcalz
jcalz

Reputation: 329418

Yes, it is possible to do what you want.

As you note, your problem is that the values of properties of ActionMapper<A> is always A. For Actions, that's a union type. What you really want to do is extract from A the constituent that matches {value: K} for each key K. Luckily, there is a pre-defined conditional type named Extract that does this for you. Let's redefine ActionMapper<A>:

type ActionMapper<A extends IAction> = {
   [K in A['value']]: Extract<A, {value: K}>;
}

Now we will try again:

const reducerMap: ActionMapper<Actions> = {
  action_one: { value: 'action_one' },
  action_two: { value: 'action_one' }, // error!
  // Type '"action_one"' is not assignable to type '"action_two"'.
}

And you get the error you expected. Hope that helps. Good luck!

Upvotes: 1

Related Questions