Template literal types in the Vuex
Tags: vue, vuex, typescript
Published: 2022-02-02 14:14:00

# 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.

```typescript
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";
```

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

# Vuex with TypeScript

```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) {},
};
```

```typescript
// 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]>;
}
```

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

```typescript
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'.
},
```

# 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:

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

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

```typescript
dispatch(
  `${module2Namespace}/actionB2` as 'actionB2',
   payload,
   { root: true },
);
```

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.

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

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

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

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

```typescript
dispatch(
  `${module2Namespace}/actionB2` as const,
   payload,
   { root: true },
);
```

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
- [https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html)
- [https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
- [https://dev.to/3vilarthas/vuex-typescript-m4j](https://dev.to/3vilarthas/vuex-typescript-m4j)