TinyTiger
TinyTiger

Reputation: 2043

Vue 3 + TypeScript: Objects returned from setup() using the JS spread operator throw an error in vscode

I have a simple Vue 3 + TypeScript repo where I am trying to integrate an Auth0 plugin.

It displays the stringified user object on the frontend, and it works as expected.

But Visual Studio Code is showing a TypeScript error Cannot find name 'user'. ts(2304) because it cannot see the object user when returned inside an ...auth spread operator.

I'm not sure why it is doing that, or how to resolve it.

This is the code for the Auth0 plugin. In a nutshell, it is using app.provide("Auth", authPlugin); to provide access to a bunch of stuff, including a user object:

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,
};

Here in my Profile.vue page I am injecting the Auth0 plugin using const auth = inject<Auth0Client>("Auth")!; and returning all of its content from setup() using the ...auth spread operator. This includes the user object which is now available to use in the template.

All of this is working on the front end. It displays the stringified user object as expected.

But vscode is throwing an Cannot find name 'user'. ts(2304) error because the user object is not explicitly returned from setup().

It seems like it doesn't know that the ...auth spread operator has the user object inside auth:

<template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(user, null, 2) }} <!-- ERROR: Cannot find name 'user'.ts(2304) -->
  </div>
</template>

<script lang="ts">
import { Auth0Client } from "@auth0/auth0-spa-js";
import { inject } from "vue";

export default {
  name: "Profile",
  setup() {
    const auth = inject<Auth0Client>("Auth")!;
    return {
      ...auth,
    };
  },
};
</script>

I have tried to solve this problem by explicitly returning a user object as shown below, but it breaks the functionality. The stringified user object is no longer displays on the front end:

<template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(auth_user, null, 2) }}
  </div>
</template>

<script lang="ts">
import { Auth0Client } from "@auth0/auth0-spa-js";
import { inject } from "vue";

export default {
  name: "Profile",
  setup() {
    const auth = inject<Auth0Client>("Auth")!;
    const auth_user = auth.getUser(); // This does not work
    //const auth_user = auth.user; // This variation also doesn't work
    return {
      auth_user,
    };
  },
};
</script>

Can anyone figure out what is going on here and how to solve the error?

Upvotes: 1

Views: 1486

Answers (2)

tony19
tony19

Reputation: 138196

There are a few issues:

  1. The Auth0Client class has no user field, so returning { ...auth } from setup() would not create a user property. But this isn't the type you want, as we see in the next point.
export default class Auth0Client {
  private options;
  private transactionManager;
  private cacheManager;
  private customOptions;
  private domainUrl;
  private tokenIssuer;
  private defaultScope;
  private scope;
  private cookieStorage;
  private sessionCheckExpiryDays;
  private orgHintCookieName;
  private isAuthenticatedCookieName;
  private nowProvider;
  cacheLocation: CacheLocation;
  private worker;
  constructor(options: Auth0ClientOptions);
  private _url;
  private _getParams;
  private _authorizeUrl;
  private _verifyIdToken;
  private _parseNumber;
  private _processOrgIdHint;
  buildAuthorizeUrl(options?: RedirectLoginOptions): Promise<string>;
  loginWithPopup(options?: PopupLoginOptions, config?: PopupConfigOptions): Promise<void>;
  getUser<TUser extends User>(options?: GetUserOptions): Promise<TUser | undefined>;
  getIdTokenClaims(options?: GetIdTokenClaimsOptions): Promise<IdToken>;
  loginWithRedirect(options?: RedirectLoginOptions): Promise<void>;
  handleRedirectCallback(url?: string): Promise<RedirectLoginResult>;
  checkSession(options?: GetTokenSilentlyOptions): Promise<void>;
  getTokenSilently(options: GetTokenSilentlyOptions & {
      detailedResponse: true;
  }): Promise<GetTokenSilentlyVerboseResponse>;
  getTokenSilently(options?: GetTokenSilentlyOptions): Promise<string>;
  private _getTokenSilently;
  getTokenWithPopup(options?: GetTokenWithPopupOptions, config?: PopupConfigOptions): Promise<string>;
  isAuthenticated(): Promise<boolean>;
  buildLogoutUrl(options?: LogoutUrlOptions): string;
  logout(options?: LogoutOptions): Promise<void> | void;
  private _getTokenFromIFrame;
  private _getTokenUsingRefreshToken;
  private _getEntryFromCache;
}
  1. While the Auth object is injected as an Auth0Client, the actual object provided in @/auth/index.ts has a type that does not overlap with Auth0Client. The actual type should be exported so that components that inject the Auth object could type the reference:
const authPlugin = {
  isAuthenticated: computed(() => state.isAuthenticated),
  loading: computed(() => state.loading),
  user: computed(() => state.user),
  getIdTokenClaims,
  getTokenSilently,
  getTokenWithPopup,
  handleRedirectCallback,
  loginWithRedirect,
  logout,
};

export type ProvidedAuthPlugin = typeof authPlugin; šŸ‘ˆ
ā‹®
app.provide("Auth", authPlugin);
  1. To enable TypeScript support in a component (including within the <template>), the component definition should be declared with defineComponent:
import { defineComponent } from "vue";

export default defineComponent({
  ā‹®
});
  1. And the Auth object's type should be used in the component when injecting it:
import type { ProvidedAuthPlugin } from "@/auth"; šŸ‘ˆ
import { inject, defineComponent } from "vue";

export default defineComponent({
  name: "Profile",
  setup() {                              šŸ‘‡
    const auth = inject("Auth") as ProvidedAuthPlugin;
    return {
      ...auth,
    };
  },
});

GitHub PR

Upvotes: 2

PawFV
PawFV

Reputation: 424

Ok for what I understand (I'm not an expert with composition API).

Here for instance in setup(), the return statement should provide you what you will have available inside the <template>.

let's say you want to use user here

 <div class="row">
    {{ JSON.stringify(user, null, 2) }} <!-- ERROR: Cannot find name 'user'.ts(2304) -->
  </div>

Basically it doesn't find any kind of user data. let's try adding it in the return statement of setup()

Try this:

<template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(user, null, 2) }}
  </div>
</template>

<script lang="ts">
import { inject, ref } from 'vue'
import { Auth0Client, User } from '@auth0/auth0-spa-js'

export default {
  name: 'Profile',
  setup() {
    /* Added for you this 2 lines, one for getting types of auth
       I think the other one is reactive */
    const auth = inject('Auth') as Auth0Client
    const user = ref<User | undefined>(undefined)

    auth.getUser().then((authuser) => (user.value = authuser))
    return {
      ...auth, // Check this one, I don't see it being used in <template>
      user // This one should be available in <template> now
    }
  }
}
</script>

Hopefully it works... Also I'm not a big fan of composition API if for whatever reason you are just learning Vue use the default API, it's a lot easier to learn and use :).

Upvotes: 0

Related Questions