'Vue 3 Inject Plugin with Composition API and TypeScript

I created a controller plugin to be used global in every component, but I can't make it work with Vue 3 + TypeScript + Composition API I get a TypeScript error

ui/plugins/controllers.ts

import { App } from 'vue'
import { provider, IProvider } from '@/core/presentation/provider'

export default {
  install: (app: App) => {
    const controllers: IProvider = provider()
    app.provide('controllers', controllers)
  }
}

main.ts

import { createApp } from 'vue'
import { controllers } from './ui'

createApp(App)
  .use(controllers)
  .mount('#app')

@/core/presentation/provider/provider.ts

import { UserController } from '../controllers'
import { IProvider } from './provider.types'

export const provider = (): IProvider => ({
  users: new UserController()
})

ui/views/Component.vue

import { onMounted, ref, inject, defineComponent } from 'vue'

export default defineComponent({
  setup() {
    const controllers = inject('controllers')

    const user = ref()
    const getUser = async () => {
      const result = await controllers.users.getById(1)
      if (result) {
        user.value = result.toJson()
      }
    }

    onMounted(getUser)

    return {
      user,
      getUser
    }
  }
})

Here I get a typescript error when I try to use the controller at this line

const result = await controllers.users.getById(1)

Error:

const controllers: unknown
Object is of type 'unknown'.Vetur(2571)

If I set the type with my interface like this I get another typescript error

import { IProvider } from '@/core'
...
const controllers: IProvider  = inject('controllers')

Error:

type 'IProvider | undefined' is not assignable to type 'IProvider'.
Type 'undefined' is not assignable to type 'IProvider'.Vetur(2322)

I can only make it work like this but I think it's weird:

const controllers: IProvider | undefined = inject('controllers')
...
const result = await controllers?.users.getById(1)


Solution 1:[1]

I managed to solve my problem with post Type-safe Vue.js Injections

One thing to note is that the inject function produces the resolved type in union with undefined. This is because there is the possibility that the injection is not resolved. It's up to you how you want to handle it.

So to deal with undefined I followed his advice and create an injectStrict function. Here is my final code:

Component.vue

import { IProvider } from '@/core'
import { injectStrict } from '@/ui/utils'
import { onMounted, ref, defineComponent } from 'vue'

export default defineComponent({
  setup() {
    const controllers: IProvider = injectStrict('controllers')
    const user = ref()
    const getUser = async () => {
      const result = await controllers.users.getById(1)
      if (result) {
        user.value = result.toObj()
      }
    }

    onMounted(getUser)

    return {
      user,
      getUser
    }
  }
})

@/utils/injections.ts

import { inject } from 'vue'

function injectStrict<T>(key: string, fallback?: T) {
  const resolved = inject(key, fallback)
  if (!resolved) {
    throw new Error(`Could not resolve ${key}`)
  }

  return resolved
}

export { injectStrict }

Solution 2:[2]

If you are sure that the injection is always resolved, you can use the Non-null assertion operator:

const controllers: IProvider = inject('controllers')! // <- notice the exclamation mark!
const result = await controllers.users.getById(1)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ocordova
Solution 2 Antonio Policastro