Search code examples
typescriptvuejs3injectvue-composition-apimutating-function

TypeScript / Vue 3: Injecting mutating function causes TypeScript error "Object is of type 'unknown'"


I’m new in TypeScript and trying to use it with Vue 3 composition API and provide / inject. Let's say in parent component A I have something like this:

// Parent component A

import { provide, ref } from 'vue';
import ScoreType from "@/types/Score";

setup() {
  ..
  const score = ref<ScoreType[]>([]);
  const updateScore = (val: ScoreType) => {
    score.value.push(val);
  };

  provide('update_score', updateScore);  
  ..
}

...and then want to inject updateScore function in child component B to be able to update values in parent component A (this is what docs recommend). Unfortunately, I get a TS error Object is of type 'unknown'

// Child component B

import { inject } from 'vue';

setup() {
  ..
  const updateScore = inject('update_score');
  const checkAnswer = (val: string) => {
    updateScore({ /* ScoreType object */ });  // → Object is of type 'unknown'.
  }
  ..
}

What should I do to fix the TypeScript error? I couldn't find any examples about injecting update functions in TS.


Solution

  • Let's firstly declare a type for our updateScore() function

    // @/types/score.ts
    export type ScoreType = { points: number };
    
    export type UpdateScoreFunction = (val: ScoreType) => void;
    

    Now we need to declare an InjectionKey which will hold the type information of our provided/injected variable (function in this case). More about it in Vue docs

    Let's make a separate folder to store our keys and to keep the things organized:

    // @/symbols/score.ts
    import { InjectionKey } from "vue";
    import { UpdateScoreFunction } from "@/types/score";
    
    export const updateScoreKey: InjectionKey<UpdateScoreFunction> = Symbol("updateScore");
    

    In our parent component (A):

    <script lang="ts">
    import { defineComponent, provide, ref } from "vue";
    
    import { ScoreType, UpdateScoreFunction } from "@/types/score";
    import { updateScoreKey } from "@/symbols/score";
    
    export default defineComponent({
      setup() {
        const score = ref<ScoreType[]>([]);
        
        // Actually, adding ': UpdateScoreFunction' is optional 
        const updateScore: UpdateScoreFunction = function (val: ScoreType) {
          score.value.push(val);
        };
    
        // Replace the string with InjectionKey
        provide(updateScoreKey, updateScore);
    
        // ...
      },
    });
    </script>
    

    In our child component (B):

    <script lang="ts">
    import { defineComponent, inject } from "vue";
    import { updateScoreKey } from "@/symbols/score";
    
    export default defineComponent({
      setup() {
    
        // Replace the string with InjectionKey
        const updateScore = inject(updateScoreKey);
    
        // In case the `updateScoreKey` is not provided by the parent component..
        if (updateScore === undefined) {
          throw new Error('Failed to inject "updateScore"');
        }
    
        const checkAnswer = (val: string) => {
    
          // ...
    
          // The error is gone
          updateScore({ 
            points: Math.floor(Math.random() * 100),
          });
        };
    
        // ...
      },
    });
    </script>
    

    Working example provided here: codesandbox.io/s/so-provide-inject