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";
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) {},
};
// 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:
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:
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.
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.
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…
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
:
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.