Home > Mobile >  Vue 3 with Typescript inject does not work as intended. Spread types may only be created from object
Vue 3 with Typescript inject does not work as intended. Spread types may only be created from object

Time:12-09

Vue 3 and TypeScript gives an error (see below) BUT only when script lang="ts" is present. Can anyone help explain why inject in Vue 3 with Typescript tag does not work?

ERROR in src/components/global/HeaderMenu.vue:85:7
TS2698: Spread types may only be created from object types.
    83 |     const auth = inject('Auth');
    84 |     return {
  > 85 |       ...auth,
       |       ^^^^^^^
    86 |     };
    87 |   },
    88 | });

Working example:

<script>
import { defineComponent } from 'vue';
import { inject } from 'vue';
export default defineComponent({
  name: 'HeaderMenu',
  inject: ['Auth'],
  methods: {
    login() {
      this.Auth.loginWithRedirect();
    },
    logout() {
      this.Auth.logout();
      this.$router.push({ path: '/' });
    },
  },
  setup() {
    const auth = inject('Auth');
    return {
      ...auth,
    };
  },
});
</script>

Example that generates the above error:

<script lang="ts">
import { defineComponent } from 'vue';
import { inject } from 'vue';
export default defineComponent({
  name: 'HeaderMenu',
  inject: ['Auth'],
  methods: {
    login() {
      this.Auth.loginWithRedirect();
    },
    logout() {
      this.Auth.logout();
      this.$router.push({ path: '/' });
    },
  },
  setup() {
    const auth = inject('Auth');
    return {
      ...auth,
    };
  },
});
</script>

Main.js

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
// Auth Service
import { Auth0 } from '@/auth';

import BootstrapVue3 from 'bootstrap-vue-3';
// Import Bootstrap an BootstrapVue CSS files (order is important)
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue-3/dist/bootstrap-vue-3.css';


async function init() {
  const AuthPlugin = await Auth0.init({
    onRedirectCallback: (appState) => {
      router.push(appState && appState.targetUrl ? appState.targetUrl : window.location.pathname);
    },
    clientId: 'xxxx',
    domain: 'xxxx',
    audience: process.env.VUE_APP_AUTH0_AUDIENCE,
    redirectUri: window.location.origin,
  });
  const app = createApp(App);
  app
    .use(AuthPlugin)
    .use(router)
    .use(BootstrapVue3)
    .mount('#app');
}

init();

Example of the auth.js

 import createAuth0Client, {
    Auth0Client,
    GetIdTokenClaimsOptions,
    GetTokenSilentlyOptions,
    GetTokenWithPopupOptions,
    LogoutOptions,
    RedirectLoginOptions,
    User
} from '@auth0/auth0-spa-js'
import {App, Plugin, computed, reactive, watchEffect} from 'vue'
import {NavigationGuardWithThis} from "vue-router";

let client: Auth0Client;

interface Auth0PluginState {
    loading: boolean,
    isAuthenticated: boolean;
    user: User | undefined,
    popupOpen: boolean;
    error: any
}

const state = reactive<Auth0PluginState>({
    loading: true,
    isAuthenticated: false,
    user: {},
    popupOpen: false,
    error: null,
})

async function handleRedirectCallback() {
    state.loading = true;

    try {
        await client.handleRedirectCallback();
        state.user = await client.getUser();
        state.isAuthenticated = true;
    } catch (e) {
        state.error = e;
    } finally {
        state.loading = false;
    }
}

function loginWithRedirect(o: RedirectLoginOptions) {
    return client.loginWithRedirect(o);
}

function getIdTokenClaims(o: GetIdTokenClaimsOptions) {
    return client.getIdTokenClaims(o);
}

function getTokenSilently(o: GetTokenSilentlyOptions) {
    return client.getTokenSilently(o);
}

function getTokenWithPopup(o: GetTokenWithPopupOptions) {
    return client.getTokenWithPopup(o);
}

function logout(o: LogoutOptions) {
    return client.logout(o);
}

const authPlugin = {
    isAuthenticated: computed(() => state.isAuthenticated),
    loading: computed(() => state.loading),
    user: computed(() => state.user),
    getIdTokenClaims,
    getTokenSilently,
    getTokenWithPopup,
    handleRedirectCallback,
    loginWithRedirect,
    logout,
}

const routeGuard: NavigationGuardWithThis<undefined> = (to: any, from: any, next: any) => {
    const {isAuthenticated, loading, loginWithRedirect} = authPlugin;

    const verify = async () => {
        // If the user is authenticated, continue with the route
        if (isAuthenticated.value) {
            return next();
        }

        // Otherwise, log in
        await loginWithRedirect({appState: {targetUrl: to.fullPath}});
    }

    // If loading has already finished, check our auth state using `fn()`
    if (!loading.value) {
        return verify();
    }

    // Watch for the loading property to change before we check isAuthenticated
    watchEffect(() => {
        if (!loading.value) {
            return verify();
        }
    })
}

interface Auth0PluginOptions {
    domain: string,
    clientId: string,
    audience: string,
    redirectUri: string,

    onRedirectCallback(appState: any): void
}

async function init(options: Auth0PluginOptions): Promise<Plugin> {
    client = await createAuth0Client({
        // domain: process.env.VUE_APP_AUTH0_DOMAIN,
        // client_id: process.env.VUE_APP_AUTH0_CLIENT_KEY,
        domain: options.domain,
        client_id: options.clientId,
        audience: options.audience,
        redirect_uri: options.redirectUri,
    });

    try {
        // If the user is returning to the app after authentication
        if (
            window.location.search.includes('code=') &&
            window.location.search.includes('state=')
        ) {
            // handle the redirect and retrieve tokens
            const {appState} = await client.handleRedirectCallback();

            // Notify subscribers that the redirect callback has happened, passing the appState
            // (useful for retrieving any pre-authentication state)
            options.onRedirectCallback(appState);
        }
    } catch (e) {
        state.error = e;
    } finally {
        // Initialize our internal authentication state
        state.isAuthenticated = await client.isAuthenticated();
        state.user = await client.getUser();
        state.loading = false;
    }

    return {
        install: (app: App) => {
            app.provide('Auth', authPlugin);
        },
    }
}

interface Auth0Plugin {
    init(options: Auth0PluginOptions): Promise<Plugin>;
    routeGuard: NavigationGuardWithThis<undefined>
}

export const Auth0: Auth0Plugin = {
    init,
    routeGuard
}

CodePudding user response:

First I think that you are doing something wrong with how you use inject (cf. https://v3.vuejs.org/guide/composition-api-provide-inject.html#using-inject). I think that you can remove the inject property.

Then it is not clear how do you use the inject properties (through the Auth injected object or through the spread properties of the Auth object).

I guess that to fix the typescript type checking by setting the type of the injected property to Auth0Plugin for example (but you will need to export the interface first).

CodePudding user response:

There is no way for a compiler to determine auth type from inject(...) call because it cannot associate it with respective provide(...) call. It needs to be provided with type information.

authPlugin type needs to be exposed, e.g.:

export type TAuthPlugin = typeof authPlugin;

Then it needs to be provided to inject. As a rule of thumb, functions like inject are generic:

const auth = inject<TAuthPlugin>('Auth');
  • Related