logaretm.com/blog/2020-1…

This article is a short introduction to provide/ Inject TypeScript usage, which is supported in Vue3 as well as @vue/ composition-API in Vue 2.

Provide Type Safety

When I first started using provide/inject in the composite API, I wrote the following code:

import { inject } from 'vue';
import { Product } from '@/types';
export default { 
  setup() { 
    const product = inject('product') asProduct; }};Copy the code

Write it this way. Do you see the problem?

When working with TypeScript, I use the AS keyword as an escape tactic if I don’t know how to make TypeScript understand the type being processed. Even though I rarely use AS, I still try to avoid using AS.

Not long ago, I tweeted on the subject: I use the composition API in a TypeScript production application and need to provide type information for provide/ Inject. I was going to do it myself, but found that Vue already had a utility type called InjectionKey

that I needed.

This means that you need a special constant file to hold the Injectable key, and then you can use the InjectionKey

to create a Symbol that contains information about the injected property type.

// types.ts
interface Product { name: string; price: number; }// symbols.ts
import { InjectionKey } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Product> = Symbol('Product');
Copy the code

The advantage of InjectionKey

is that it works both ways. It provides type-safety, which means that if you try to provide an incompatible value with that key, TypeScript will report an error:

import { provide } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ Argument of type 'string' is not assignable to...
provide(ProductKey, 'this will not work');
/ / ✅
provide(ProductKey, {
  name: 'Amazing T-Shirt'.price: 100});Copy the code

On the receiving side, your inject will also be entered correctly:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // typed as Product or undefined
Copy the code

One thing to note is that the Inject function generates the parse type combined with undefined. This is because it is possible that the component is not injected. It depends on how you want to deal with it.

To eliminate undefined, pass a default value to the Inject function. The cool thing here is that the defaults are also type-checked:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
// ⛔️ Argument of type 'string' is not assignable to...
const product = inject(ProductKey, 'nope');
// ✅ Type checks out
const product = inject(ProductKey, { name: ' '.price: 0 });
Copy the code

Provide the value of the response

While you can provide common value types, they are not commonly used because we often need to react to changes in these values. You can also create reactive injections using generic types.

For reactive references created using ref, you can use the generic ref type to enter your InjectionKey, so it is a nested generic type:

// types.ts
interface Product {
  name: string;
  price: number;
}
// symbols.ts
import { InjectionKey, Ref } from 'vue';
import { Product } from '@/types';
const ProductKey: InjectionKey<Ref<Product>> = Symbol('Product');
Copy the code

Now, when you get the component inject content via ProductKey, it will have Ref type or undefined, as we discussed earlier:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey); // typed as Ref<Product> | undefinedproduct? .value;// typed as Product
Copy the code

Deal with undefined

I’ve already mentioned how to handle undefined with normal values, but you can’t provide a safe default value for complex and reactive objects.

In our example, we are trying to resolve a Product context object, and if the injection does not exist, there could be a potentially more serious problem, and an error could occur if the injection is not found.

Vue displays a warning by default if the injection is not resolved, Vue can choose to throw an error if the injection is not found but Vue cannot assume whether an injection is needed or not, it is up to you to understand the value of resolve injection and undefined.

For optional injection properties, just provide a default value, or if it’s not that important, you can also use the optional link operator:

import { inject } from 'vue';
import { CurrencyKey } from '@/symbols';
const currency = inject(CurrencyKey, ref('$'));
currency.value; // no undefined
// or
constcurrency = inject(CurrencyKey); currency? .value;Copy the code

But for requirements like our Product type, we can do something simple like this:

import { inject } from 'vue';
import { ProductKey } from '@/symbols';
const product = inject(ProductKey);
if(! product) {throw new Error(`Could not resolve ${ProductKey.description}`);
}
product.value; // typed as `Ref<Product>`
Copy the code

Throwing errors is a way to leverage TypeScript type checking features. Because we dealt with the undefined component earlier, the code won’t work until the last line if we don’t add the product type when we actually use it.

To be more reusable, let’s create a function called injectStrict that does all this for us:

function injectStrict<T> (key: InjectionKey
       
        , fallback? : T
       ) {
  const resolved = inject(key, fallback);
  if(! resolved) {throw new Error(`Could not resolve ${key.description}`);
  }
  return resolved;
}
Copy the code

Now you can use it directly instead of inject and you’ll get the same security in a modular way without having to deal with the pesky undefined:

import { injectStrict } from '@/utils';

const product = injectStrict(ProductKey);

product.value; // typed as `Product`
Copy the code

conclusion

I think provide/inject will become more popular, especially with composition apis, and knowing their TypeScript capabilities will make your code easier to maintain and safer to use.

Thanks for reading! 👋