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.
Top comments (0)