DEV Community

Meeooow
Meeooow

Posted on

Types Problem: Nuxt, Vuetify with Vue Composition API

Hi, everybody! This is my first article in Dev.to and I'm really happy to be able to share my recent experiences using Nuxt.js and Vue Composition API.

I'm currently working on a small toy project based on Nuxt. This project uses the following skill base.

  • Nuxt.js
  • Typescript
  • Vuetify
  • Storybook

In addtion, I added the Vue Composition API that will be used in Vue3. However, there ware some problems in the environment using Nuxt and Typescript.

So let's get started! What problems were there and how to solve them.

Nuxt ComponentOptions

A Nuxt.js provides a variety of component options and if you using Typescript, you can find component options in @nuxt/types

// node_modules/@nuxt/types/app/vue.d.ts

/**
 * Extends interfaces in Vue.js
 */

import Vue from 'vue'
import { MetaInfo } from 'vue-meta'
import { Route } from 'vue-router'
import { Context, Middleware, Transition, NuxtApp } from './index'
import { NuxtRuntimeConfig } from '../config/runtime'

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    asyncData?(ctx: Context): Promise<object | void> | object | void
    fetch?(ctx: Context): Promise<void> | void
    fetchDelay?: number
    fetchOnServer?: boolean | (() => boolean)
    head?: MetaInfo | (() => MetaInfo)
    key?: string | ((to: Route) => string)
    layout?: string | ((ctx: Context) => string)
    loading?: boolean
    middleware?: Middleware | Middleware[]
    scrollToTop?: boolean
    transition?: string | Transition | ((to: Route, from: Route | undefined) => string | Transition)
    validate?(ctx: Context): Promise<boolean> | boolean
    watchQuery?: boolean | string[] | ((newQuery: Route['query'], oldQuery: Route['query']) => boolean)
    meta?: { [key: string]: any }
  }
}

declare module 'vue/types/vue' {
  interface Vue {
    $config: NuxtRuntimeConfig
    $nuxt: NuxtApp
    $fetch(): void
    $fetchState: {
      error: Error | null
      pending: boolean
      timestamp: number
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

But, when you using the Vue Composition API in Nuxt components, the basic Types scope chagnes from @nuxt/types to @vue/composition-api

Therefore, we can not use the types for some component options that only nuxt has like the layout, middleware, fetch

Let's see an example.

<template>
  <div>Hello Vue Composition API!</div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'

export default defineComponent({
    layout: 'some-layout' // Error: No overload matches this call
})
</script>
Enter fullscreen mode Exit fullscreen mode

Basically, to use composition-api in the Typescript environment, we declare the definedComponent.

If we want to use the layout property, we have to declare it in definedComponent, but you will see an error(or warning) that the type cannot be found in the IDE or Editor.

In this situation we can infer why the layout is not avaliable.

// node_modules/@vue/composition-api/dist/index.d.ts

import Vue$1, { VueConstructor, ComponentOptions, VNode, CreateElement } from 'vue';

...

interface ComponentOptionsBase<Props, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}> extends Omit<ComponentOptions<Vue, D, M, C, Props>, 'data' | 'computed' | 'method' | 'setup' | 'props'> {
    data?: (this: Props, vm: Props) => D;
    computed?: C;
    methods?: M;
}

...

declare type ComponentOptionsWithoutProps<Props = unknown, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}> = ComponentOptionsBase<Props, D, C, M> & {
    props?: undefined;
    setup?: SetupFunction<Props, RawBindings>;
} & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>;

...

declare function defineComponent<RawBindings, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}>(options: ComponentOptionsWithoutProps<unknown, RawBindings, D, C, M>): VueProxy<unknown, RawBindings, D, C, M>;
declare function defineComponent<PropNames extends string, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions>(options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>): VueProxy<Readonly<{
    [key in PropNames]?: any;
}>, RawBindings, D, C, M>;
declare function defineComponent<Props, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions>(options: HasDefined<Props> extends true ? ComponentOptionsWithProps<PropsOptions, RawBindings, D, C, M, Props> : ComponentOptionsWithProps<PropsOptions, RawBindings, D, C, M>): VueProxy<PropsOptions, RawBindings, D, C, M>;
Enter fullscreen mode Exit fullscreen mode

Yes! We found it! The problem is that definedComponent only support default Vue ComponentOptions types. So, How can we solve this problem?

vue-shims.d.ts

First, create a file vue-shim.d.ts in the @types folder at project root. (If you have seen this documentation, vue-shim.d.ts will already exist.)

import Vue from 'vue'

import { Context, Middleware } from '@nuxt/types'

...

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
        fetch?(ctx: Context): Promise<void> | void
    layout?: string | ((ctx: Context) => string)
        middleware?: Middleware | Middleware[]
  }
}
Enter fullscreen mode Exit fullscreen mode

And, Like the above code, declare ComponentOptions interface as extends Vue in the 'vue/types/options' module.

Internally, this declaration has the following meaning::

  1. vue-shim.d.ts extends a default ComponentOptions of Vue
  2. definedComponent extends the new ComponentOptions interface declared in step1
  3. We can use newly added types in definedComponent.

Good! Now we can use the types of Nuxt.js ComponentOptions!

$vuetify

Vuetify is a Material Design component framework for Vue.js

Vuetify has similar issues with types like ComponentOptions in Nuxt and Composition environment. That is, we can not access the type of this.$vuetify in the definedComponent.

Maybe, If you use Vueitfy in Nuxt.js, you will be using the @nuxtjs/vuetify

@nuxtjs/vuetify provides type of $vuetify in Nuxt Context as follows:

// node_modules/@nuxtjs/vuetify/dist/index.d.ts

import { Module } from '@nuxt/types';
import { Framework } from 'vuetify';
import { Options, TreeShakeOptions, VuetifyLoaderOptions } from './options';
declare module '@nuxt/types' {
    interface Configuration {
        vuetify?: Options;
    }
    interface Context {
        $vuetify: Framework;
    }
}
declare const vuetifyModule: Module<Options>;
export { Options, TreeShakeOptions, VuetifyLoaderOptions };
export default vuetifyModule;
Enter fullscreen mode Exit fullscreen mode

This problem can also be solved by declaring a new type like the above problem.

// vue-shim.d.ts

import { Framework } from 'vuetify'

...

declare module 'vue/types/vue' {
  interface Vue {
    $vuetify: Framework
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the $vuetify type is also available like this!

<script lang="ts">
import { defineComponent } from '@vue/composition-api'

export default defineComponent({
  setup(_, context) {
        ...
    const { width } = context.root.$vuetify.breakpoint
        ...
  }
})
</script>
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using the Nuxt.js and Vue Composition API together can be a very good option. However, the Composition API is not yet fully supported for Nuxt (especially TypeScript).

Of course, the content of this article is not all, but I hope it helps people who want to use Nuxt and Composition API in a Typescript environment.

If you are more interested in this topic, check out nuxt-community/composition-api project!

Thank you!

Top comments (1)

Collapse
 
carlomigueldy profile image
Carlo Miguel Dy

For updates regarding nuxt composition api, you now have the other properties available on defineComponent() like layout, middleware, etc

Also for accessing $vuetify, just use the useContext() method and you will have access to $vuetify from it

const context = useContext()
const breakpoint = ({
breakpoint: computed(() => context.$vuetify.breakpoint)
})

It worked for me