Template literal types in the Vuex

przemyslawjanbeigert

Przemyslaw Jan Beigert

Posted on February 2, 2022

Template literal types in the Vuex

Introduction

TypeScript 4.1 introduced template literal types. On the first look it doesn’t sound interesting, it allows the creation of a union of literal types based on other ones.

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
// same as
//   type SeussFish = "one fish" | "two fish"
//                  | "red fish" | "blue fish";
Enter fullscreen mode Exit fullscreen mode

However there’s couple of cases when this feature is very useful.

Vuex with TypeScript

// module1.mutations.ts
interface Module1Mutations {
  mutationA1(state: Module1State, payload: MutationA1Payload): void
  mutationA2(state: Module1State, payload: MutationA2Payload): void
}

export const module1Mutations: MutationTree<Module1State> & Module1Mutations = {
  mutationA1(state, payload) {},
  mutationA2(state, payload) {},
};
Enter fullscreen mode Exit fullscreen mode
// module1.actions.ts
interface Module1Actions {
  actionA1(context: Module1ActionContext, payload: ActionA1Payload): void
  actionA2(context: Module1ActionContext, payload: ActionA2Payload): void
}

type Module1ActionContext = {
  dispatch<K extends keyof Module1Actions>(
     actionType: K,
     payload: Parameters<Module1Actions[K]>[1],
     options?: DispatchOptions,
  ): ReturnType<Module1Actions[K]>;
  commit<K extends keyof Module1Mutations>(
     actionType: K,
     payload Parameters<Module1Mutations[K]>[1]]
  ): ReturnType<Module1Mutations[K]>;
}
Enter fullscreen mode Exit fullscreen mode

After that TypeScript will throw compilation error when someone will dispatch/commit action/mutation with a wrong payload. e.g:

actionA1({ commit, dispatch }, payload) {
   commit('mutationA1', false); // ERROR: Argument of type 'boolean' is not assignable to parameter of type 'MutationA2Payload'.
   dispatch('actionA2', false); // ERROR: Argument of type 'boolean' is not assignable to parameter of type 'ActionA2Payload'.
},
Enter fullscreen mode Exit fullscreen mode

Dispatching actions from another module

But what about the case when we need to dispatch action from module2? Module1ActionContext doesn’t know about actions and mutations from another namespace. To let him know we need to add something like this:

type Module1ActionContext = {
 ...
 dispatch<K extends keyof Module2Actions>(
   actionType: keyof Module2Actions,
   payload?: Parameters<Module2Actions[K]>[1],
   options?: DispatchOptions,
): Promise<void> | void;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Sounds good but we have to call dispatch with ${module2Namespace}/actionB2 not a actionB2. So best we ca do is type cast.

dispatch(
  `${module2Namespace}/actionB2` as 'actionB2',
   payload,
   { root: true },
);
Enter fullscreen mode Exit fullscreen mode

Looks like type safe code however ${module2Namespace}/actionB2 as 'actionB2' is a duplication, compiler should knows which actions we are dispatching. Also we have to remember about { root: true } because TS wouldn’t throw error when this parts is missing.

With template literal types

After update TypeScript to 4.1+ (and prettier to 2.0+) we are allow to declare context like this.

type Module1ActionContext = {
 ...
 dispatch<K extends keyof Module2Actions>(
   actionType: `module2Namespace/${keyof Module2Actions}`,
   payload?: Parameters<Module2Actions[K]>[1],
   options?: DispatchOptions,
): Promise<void> | void;
  ...
}
Enter fullscreen mode Exit fullscreen mode

This syntax means we’re mapping union actionB1 | actionB2 into module2Namespace/actionB1 | module2Namespace/actionB2. It’s almost perfect, almost…

dispatch(
  `${module2Namespace}/actionB2`,
   payload,
   { root: true },
); // ERROR: Argument of type 'string' is not assignable to parameter of type '"module2Namespace/actionB1" | ""module2Namespace/actionB1"'
Enter fullscreen mode Exit fullscreen mode

By default value like this ${module2Namespace}/actionB2 is typed as string not a literal. To change that we can use as const:

dispatch(
  `${module2Namespace}/actionB2` as const,
   payload,
   { root: true },
);
Enter fullscreen mode Exit fullscreen mode

So we changed as actionB2 into as const. Great success? Yes because with as const there’s no duplication and you can not make bug like this: dispatch({module2Namespace}/actionB2 as actionB1, actionB1Payload) with payload from actionB1 without notice that in the compilation message.

Conclusion

This is not first TypeScript feature which looks silly but with time become useful.

Links

💖 💪 🙅 🚩
przemyslawjanbeigert
Przemyslaw Jan Beigert

Posted on February 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related