Extending Vue instance interface in TypeScript Quasar boot file

2.2k Views Asked by At

Edit: I said several times in the post below that I'm "not in a module". Any time JavaScript code in one file is called from another that's technically a module. What I meant to say was that this module is called automatically by Quasar at script initialization time and is expected to have global side effects. The module is never imported into any code that I write.

Succint post:

I'm using Quasar 2.0.0 with TypeScript support and Vue Class Components. I wish to add an instance property to be accessible in my classes as: this.$fetch(...);

I've created a boot file with the following:

import Vue from "vue";

declare module "vue/types/vue"
{
    interface Vue
    {
        prototype: {
            $fetch: (resource: (string | Request), init?: RequestInit) => Promise<Response>
        },
        $fetch: (resource: (string | Request), init?: RequestInit) => Promise<Response>
    }
}

type Context = {
    Vue: Vue
};

export default async ({ Vue: vue }:Context) =>
{
    vue.prototype.$fetch = async (resource, init) =>
    {
        return fetch(resource, init);
    };
};

This doesn't throw any errors with TypeScript. But when I try to use it in a component, I get an error:

<template>
    <q-layout>
        <q-page-container>
            <q-page class="flex flex-center">
                <h1>Test</h1>
            </q-page>
        </q-page-container>
    </q-layout>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";

@Component
export default class TestPage extends Vue
{
    mounted ()
    {
        this.$fetch("https://www.google.com");
    }
}
</script>

Property '$fetch' does not exist on type 'TestPage'.

Previous (rambly) post:

I'm working on a Quasar app and as a convenience I'd like to create a modified fetch method that injects my API key if the requested domain matches my back-end domain. The interface would be something like:

@Component
export default class VueComponent extends Vue
{
    mounted()
    {
        this.$fetch("https://www.example.com/api/endpoint").then(console.log);
    }
}

I know how to define something like this. I just need to update the Vue prototype, per https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html:

Vue.prototype.$fetch = async function(request, init)
{
    // ... blah ...
};

Per Quasar's documentation, the proper time to update the Vue prototype is in a boot file. So I created one:

quasar new boot fetch

My boot file started out fairly simple:

export default async ({ Vue }) =>
{
    Vue.prototype.$fetch = async (resource, init) =>
    {
        return fetch(resource, init);
    };
};

When everything fell apart is when I tried to convert to TypeScript. The parameter Vue implicitly has an any type, because there's no type provided to the parameter object.

I tried defining the type like so:

import Vue from "vue";

type Context = {
    Vue: Vue
};

export default async ({ Vue }:Context) =>

However this caused complains about variable shadowing (Vue is defined in the upper scope). I noticed that the Vue variable had a type of VueConstructor<Vue>, so I instead tried:

import { VueConstructor } from "vue";

type Context = {
    Vue: VueConstructor
};

export default async ({ Vue }:Context) =>

That worked for the function definition, but threw an error on the Vue.prototype.$fetch = ... line: "Unsafe member access .$fetch on any value". TypeScript doesn't expect Vue.prototype to have a $fetch property. If I were making a module definition I would extend the interface like so:

declare module "abc" {
    VueConstructor: {
        prototype: {
            $fetch: ...
        }
    }
}

However I'm not declaring a module -- this is a boot file. I tried a quick hack to work around this, defining my own VueConstructor only within the context of my boot file:

interface VueConstructor
{
    prototype: {
        $fetch: (resource: (string | Request), init?: RequestInit) => Promise<Response>
    }
}

type Context = {
    Vue: VueConstructor
};

export default async ({ Vue }:Context) =>
{
    Vue.prototype.$fetch = async (resource: (string | Request), init?: RequestInit) =>
    {
        return fetch(resource, init);
    };
};

This finally got TypeScript to stop complaining about this file, but the moment I try to utilize this.$fetch in a component:

Property '$fetch' does not exist on type 'VueComponent'.

Since VueComponent extends Vue I think I need to update the definition of the Vue class, but I'm lost on how to do this. I tried adding the following to my boot file:

interface Vue
{
    $fetch: (resource: (string | Request), init?: RequestInit) => Promise<Response>
}

But this didn't work. I can't just use declare module because, again, I'm not in a module. How do you extend a type globally without a module?

1

There are 1 best solutions below

0
On BEST ANSWER

The code example in my "succinct post" (showing what I came up with after trial and error) actually works -- I just had to restart my computer (possibly just had to restart my IDE but hitting close wasn't fully shutting it down for some reason)