Nuxt.jsでTypeScriptを使うために色々試して諦めた
最近色々あってあんまりコードが書けてなかったけど、これじゃあかんと思って再開した。
とりあえず、アツいと思ってるものをやろうかな〜ということで、VueFesで話を聞いてからNuxt熱が再燃してきたので、ibuosをNuxtで書き換えてみよう、というのを試していた。
React&MobX&TypeScriptからの置き換え
もともとがReact&MobX&TypeScriptで動いている。これを書き換えたい。
Nuxtを使う以上、
- React -> Vue
- MobX -> Vuex
は確定として、TypeScriptをどうするか?という問題が残った。
正直、TypeScript&VSCodeでコードを書く体験を一度味わうと、もうただのPureJSには戻れない感がある。
ということで、NuxtでTypeScriptを使うために色々頑張ってみた。
最終的にはタイトルにある通り諦めたのだが、これを再度構築するの結構ツラいし時間がかかると予想されたので、いつか役に立つかもしれないのでここに残しておく。
型定義を書く
- Vueコンポーネント内のthisからの推論
- 頻繁に登場するNuxtContext
- axiosモジュール
あたりは自分で型定義ファイルを用意した。 @fukuiretuさんの記事やGistをだいぶ参考にさせてもらった。感謝。
- 某アイシングクッキー屋のサイトを、アイシングクッキー界一ナウい技術スタックなサイトにリニューアルした話|福井 烈 / piece of cake, inc.|note
- Nuxt.jsで必要なTypeScriptの型定義 · GitHub
最終的にはこんなのが仕上がった。
import Vue from 'vue'; import { Route } from 'vue-router'; import { Store } from 'vuex'; import { AxiosInstance, AxiosRequestConfig, AxiosPromise } from 'axios'; interface NuxtAxiosInstance extends AxiosInstance { $request<T = any>(config: AxiosRequestConfig): AxiosPromise<T>; $get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>; $delete<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>; $head<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>; $options<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>; $post<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): AxiosPromise<T>; $put<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): AxiosPromise<T>; $patch<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): AxiosPromise<T>; } interface NuxtContext { isClient: boolean; isServer: boolean; isStatic: boolean; isDev: boolean; isHMR: boolean; route: Route; store: Store<any>; env: object; query: object; nuxtState: object; req: Request; res: Response; params: { [key: string]: any }; redirect: (path: string) => void; error: (params: { statusCode?: String; message?: String }) => void; beforeNuxtRender: (param: { Conmponents: any; nuxtState: any }) => void; $axios: NuxtAxiosInstance; } declare module 'vue/types/options' { interface ComponentOptions<V extends Vue> { layout?: string; middleware?: string | String[]; fetch?: (context: NuxtContext) => void; asyncData?: (context: NuxtContext) => void; scrollToTop?: boolean; transition?: string | object | Function; validate?: (context: NuxtContext) => boolean; head?: () => { [key: string]: any }; watchQuery?: string[]; } }
declare module '*.vue' { import Vue, { ComponentOptions } from 'vue'; const value: ComponentOptions<Vue>; export default value; }
Vuexで型定義
TypeScript力が足りてないので、正直これであってるのかわからないが、こんな感じになった。 (コードは雰囲気で抜粋してる)
State
export interface State { user: User; } export const state = (): State => ({ user: { id: null, displayName: '', } });
Getter
import { GetterTree, ActionTree, MutationTree } from 'vuex'; export const getters: GetterTree<State, any> = { isSignin: (state: State) => state.user.id };
Action
import { GetterTree, ActionTree, MutationTree } from 'vuex'; export const types = { SET_DISPLAY_NAME: 'SET_DISPLAY_NAME', }; export const actions: ActionTree<State, any> = { async updateDisplayName(context, name: string): Promise<void> { await (this.$axios as NuxtAxiosInstance).$patch('/myself/name', { name }); context.commit(types.SET_DISPLAY_NAME, name); }, }
Mutation
import { GetterTree, ActionTree, MutationTree } from 'vuex'; export const types = { SET_DISPLAY_NAME: 'SET_DISPLAY_NAME', }; export const mutations: MutationTree<State> = { [types.SET_DISPLAY_NAME](state, displayName: string) { state.user.displayName = displayName; } }
だいぶ意味不明な感じにはなってる。
- vuexの XXXTree って使って大丈夫なのか?
this.$axios
とかを型付けする方法がさっぱりわからない
といった悩みを抱えていたが、とりあえずある程度それっぽくなったのでこれで妥協した。
Vueファイル
たとえば、とある pages/ 配下のファイルからscript部を一部抜粋すると
import Vue from 'vue'; import Component from 'vue-class-component'; import { namespace } from 'vuex-class'; import { State as Auth } from '../store/auth'; import * as auth from '~/store/auth'; import { NuxtContext } from 'types'; const authModule = namespace(auth.name); @Component({ head: () => ({ title: 'マイページ' }), }) export default class MyPage extends Vue { @authModule.State user!: auth.User; @authModule.Action updateDisplayName: any; name = ''; asyncData(context: NuxtContext) { const user: auth.User = context.store.state.auth.user; if (!user.id) { return context.redirect( `/signin?r=${encodeURIComponent(context.route.path)}` ); } return { name: user.displayName }; } async handleUpdate() { await this.updateDisplayName(this.name); } }
こんなことになった。
Propsなどを受け取るコンポーネント等の場合は、こんな感じ。
import Vue from 'vue'; import Component from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; import { ellipsis } from '~/lib/text'; @Component export default class EllipsisText extends Vue { @Prop({ default: '' }) text!: string; @Prop({ default: 100 }) size!: number; get ellipsisText() { return ellipsis(this.text, this.size); } }
いずれにしても、
- vue-class-component
- vuex-class
- vue-property-decorator
あたりを駆使して、うまく型情報を渡していくような書き方になる。
ずいぶん普通にVueを使う場合とは書き方が違うが、 @Props
とかは解りやすくて良いな〜と思いながら書いてた。
ESLint & Prettier
正直色々いじりすぎて何のプラグインを入れたか記憶が薄いが、create-nuxt-appを叩いたときに作られる .eslintrc.js
をもとに色々いじって、最終的にこうなった。
module.exports = { root: true, env: { browser: true, node: true }, parserOptions: { parser: 'typescript-eslint-parser', }, extends: [ // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 'typescript', 'plugin:vue/essential', 'plugin:prettier/recommended' ], // required to lint *.vue files plugins: [ 'vue', 'prettier' ], // add your custom rules here rules: { 'typescript/no-var-requires': false, 'typescript/no-non-null-assertion': false, 'typescript/explicit-function-return-type': false, 'typescript/no-angle-bracket-type-assertion': false } }
確かこのあたりを使うのがポイントだったと思う。
- eslint-plugin-typescript
- eslint-config-prettier
- eslint-plugin-prettier
- typescript-eslint-parser
ビルド時に error TypeError: Cannot set property 'ts' of undefined
が出る対策
こちらのissueを参考にして解決。 Nuxt Edge - Issue when running/building the project · Issue #48 · nuxt-community/typescript-template · GitHub
webpackにts-loaderを通してあげる。
front/modules/typescript.js
module.exports = function() { // Add .ts & .tsx extension to Nuxt this.nuxt.options.extensions.push('ts', 'tsx'); // Extend webpack build this.extendBuild(config => { // Add TypeScript config.module.rules.push({ test: /\.tsx?$/, loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/] }, }); // Add .ts extension in webpack resolve if (!config.resolve.extensions.includes('.ts')) { config.resolve.extensions.push('.ts'); } // Add .tsx extension in webpack resolve if (!config.resolve.extensions.includes('.tsx')) { config.resolve.extensions.push('.tsx'); } }); };
front/nuxt.config.js
module.exports = { ... modules: ['~/modules/typescript.js'], ... }
結果
Nuxt.jsで、それなりに型で守られつつ、ESLint/Prettierでコードもきれいに書いていくことが出来るようになった。
正直、いろんなところで躓いて、ここに辿り着くまでに5〜6時間ハマり続けていた。
その後コードを書き進めて...
ここまで頑張ったのに結局かよって感じですが、最終的には、TypeScriptを併用するのはあきらめることにした。
大きい理由は2点ある。
記述が増えすぎてキツかった
特にこれはVuex周りで思った。Vuexはサクサク書けるな〜という印象を持ってたんだけど、型で守ろうとすると記述がすさまじく増える感じがあった。
型付けのための記述のクセが強い
型を付けようとした際に、クラスコンポーネントやデコレータといったものを駆使して、いかにTypeScriptに型情報を教えてあげるか、といったところがポイントになってくる。これ自体は凄くて、書いてみると「おぉ... Vueに秩序が..!!!」という感動も覚えた。
ただ、ECMAScriptを使うような、いわゆる良く書くいつものSFCとだいぶ記述の差異が大きい点は考えないといけなくて、しばらく時間をあけてからコードを見たときに、「俺の知ってるVueと違う!」ってなってしまった。(私自身の記憶力とかの問題も大きい。)
便利ではあるんだけど、標準のレールから結構外れるような感覚を覚えて、これをずっと面倒見ていける自信が沸かなくなってしまった。
とはいえ、これはあくまでも個人の、しかもほぼ誰もアクセスしてこないような小さい趣味サイトの開発するときの一人の感想でしかない。
ある程度の規模であったり、サービス・プロダクトの性質によっては、恩恵がコストを上回ることも十分考えられそうなので、お仕事で使う場合には様々な事情を考慮した上で吟味しないといけないな、と思った。(これはNuxtやTypeScriptに限った話ではないので、常日頃から意識していきたい。)
というわけで、燃え尽きて終了です。