Types Problem: Nuxt, Vuetify with Vue Composition API
Tags: typescript, nuxt, vuetify, vue
Published: 2020-09-04 14:35:21

Hi, everybody! This is my first article in [Dev.to](http://dev.to) and I'm really happy to be able to share my recent experiences using [Nuxt.js](https://github.com/nuxt/nuxt.js) and [Vue Composition API](https://github.com/vuejs/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`

```typescript
// 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
    }
  }
}
```
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.
```typescript
<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>
```
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.
```typescript
// 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>;
```

**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](https://typescript.nuxtjs.org/guide/setup.html#configuration),  `vue-shim.d.ts` will already exist.)

```typescript
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[]
  }
}
```
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](https://github.com/nuxt-community/vuetify-module)

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

```typescript
// 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;
```
This problem can also be solved by declaring a new type like the above problem.

```typescript
// vue-shim.d.ts

import { Framework } from 'vuetify'

...

declare module 'vue/types/vue' {
  interface Vue {
    $vuetify: Framework
  }
}
```
Now the `$vuetify` type is also available like this!

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

export default defineComponent({
  setup(_, context) {
		...
    const { width } = context.root.$vuetify.breakpoint
		...
  }
})
</script>
```
## 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](https://github.com/nuxt-community/composition-api) project!

Thank you!