Typing Vue inject with object destructuring
I recently started using the Composition API from Vue.js and I struggled to use typings with the Provide / Inject functionality.
Let's say I have the following components code and the injector component is a descendent of the provider one.
- Provider component
<script lang="ts" setup>
import Injector from './Injector.vue';
import { provide, ref, type Ref } from 'vue';
const active: Ref<boolean> = ref(true);
const updateActive = (value: boolean): void => {
active.value = value;
}
provide('activeProvider', {
active,
updateActive
});
</script>
<template>
<p>This is the Provider component.</p>
<Injector />
</template>
- Injector component
<script lang="ts" setup>
import { inject } from 'vue';
const { active, updateActive } = inject('activeProvider');
</script>
<template>
<p>This is the Injector component.</p>
<button @click="updateActive(!active)">Toggle active</button>
<pre>{{ active }}</pre>
</template>
Because I didn’t define a type for what the inject
function returns, I would see the following errors in the code editor:
Property ‘active’ does not exist on type ‘unknown’.
Property ‘updateActive’ does not exist on type ‘unknown’.
The inject
function has the following signatures:
// signature 1
function inject<T>(key: InjectionKey<T> | string): T | undefined;
// signature 2
function inject<T>(key: InjectionKey<T> | string, defaultValue: T, treatDefaultAsFactory?: false): T;
In the first signature, I can provide a Typescript generic to the inject
function, but the return type can also be undefined
. That is because there is no guarantee that this value will be provided at runtime.
For example, lets define an Active
interface.
import type { Ref } from 'vue';
export default interface Active {
active: Ref<boolean>
updateActive: (value: boolean) => void
}
And use it in the inject
function.
import Active from '@/types/Active';
const { active, updateActive } = inject<Active>('activeProvider');
If I use object destructuring there is a possibility of trying to extract properties from a undefined
value, which would result in the following errors in the code editor:
Property ‘active’ does not exist on type ‘Active | undefined’.
Property ‘updateActive’ does not exist on type ‘Active | undefined’.
To fix this, I could:
- Assign a default value using the second parameter (signature 2)
import Active from '@/types/Active';
const { active, updateActive } = inject<Active>('activeProvider', {
active: false,
updateActive: () => undefined
});
Bear in mind that if for some reason the value is not provided at runtime, no warnings/errors will appear when the component is loaded. Also, post interactions on those values won’t have any effect. In many cases, it is best to have visibility on the error to easily address it beforehand, therefore this can be a disadvantage.
2. Force cast the returned value
import Active from '@/types/Active';
const { active, updateActive } = inject('activeProvider') as Active;
This way I'm explicitly eliminating the undefined
return possibility. And if for some reason, the value is not provided at runtime, I would see a warning [Vue warn]: injection “activeProvider” not found.
in the console. Also, the destructuring would fail.
Uncaught (in promise) TypeError: Cannot destructure property ‘active’ of ‘inject(…)’ as it is undefined.
Using InjectionKey
In the inject
function signature you'll notice that the first parameter can be a string
or an InjectionKey
type, which has some advantages:
- Avoid potential name collisions
- Sync the type of the injected value between the provider and the injector
Here is an example:
First export the injection keys from another file:
import type { InjectionKey } from 'vue';
import type Active from '@/types/Active';
export const activeKey = Symbol() as InjectionKey<Active>;
And use it in the provider.
import { activeKey } from '@/types/injectionKeys';
import { provide, ref, type Ref } from 'vue';
const active: Ref<boolean> = ref(true);
const updateActive = (value: boolean): void => {
active.value = value;
}
provide(activeKey, {
active,
updateActive
});
An error occurs if the provide
function receives an invalid payload.
And the inject
function is already typed.
Notice that, because the value can still be undefined
, if object destructuring is used, it is still necessary to add a default value or force cast.
import { activeKey } from '@/types/injectionKeys';
import type Active from '@/types/Active';
import { inject } from 'vue';
const { active, updateActive } = inject(activeKey) as Active;