When using Vue 3 and TypeScript, an error is encountered (as shown below) only when the script lang="ts" attribute is present. Can someone provide insight into why the inject
function in Vue 3 with TypeScript does not function correctly?
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
}