はじめに
Vue #2 Advent Calendar 2019の8日目です。
本日は@watsuyo_2が「Vue.js+TypeScriptのプロジェクトで型推論をするTIPS」を記事にします!
フロントエンドエンジニア1年目を振り返りながら、これからVue.jsを学ぶ人や業務経験は浅いがVue.jsにTypeScriptを導入していきたい方が分かりやすいようにまとめています。
普段、 Nuxt.js(Vue.js) と Firestore を使用した開発をしているので例としてでてきますがご了承ください。
↓は1年前に転職するまでの流れを書いた記事にあります。
新年からWebエンジニアになる僕に2018年、起こったこと
前提条件
Vue.extendを使う
Vue.jsでTSを書く場合、Vue.extend、vue-class-component、vue-property-decoratorといった選択肢がVue.jsにはありますが、今回はVue.extendを採用した事例になります。
好みの問題でもありますが、素に一番近い点が自分は気に入っています。
any型という逃げ道
TypeScriptを使う実際の業務では
「一旦はanyにしておこう」
「型も大事だけど機能実装を優先しよう」
といったケースは多いかと思います。
実際には開発初期段階でany型の宣言を書いてしまったことによって、予期せぬ値が変数や関数の引数に入ることに気づかなかったり、後でやれば大丈夫と思っていても誰かがやるだろうと放置したり、any型を定義していたことすら忘れてしまう可能性があります。
確かに、開発速度を上げて機能実装していくことによるメリットも開発初期段階では無くはないですが、型定義をおざなりにした代償としてリファクタにかける工数や、開発メンバーが増えてきた時にコードの治安を一定値まで保つことも難しくなります。
やらないほうがいいことと型推論方法
1. map〇〇系のヘルパー関数の利用
- mapState
- mapMutations
- mapGetters
- mapActions
のようにcomponentからstoreにアクセスするために使用する関数です。
ただヘルパー関数を使用すると全てany型として処理されます。
前提として、直接StateやMutationにアクセスすることは、秩序を守る意味でもVuexライフサイクル内での型定義を固める意味でも避けて開発をしています。
そのため、TypeScriptを使用する場合は、computed内でstoreからのstateをgetters経由で取得し、return値に型を定義します。
例えば、stateにある全てのtods配列を取得する場合に以下のようなcomputedを書きます。
// ここはvuex側では無く、componentで使用する型を書いたファイルをimportする
import { TodoData } from '~/types/firestore'
~略~
computed: {
todos: TodoData[] {
return this.$store.getters['todo/all']
}
}
こうすることで、todos
をthis
で参照する場合に型推論が効きます。
2. this.$store.stateで直接stateを参照する
Vuexのライフサイクルを無視した、stateの参照は秩序を乱すだけでなく、型推論も効かないためオススメできません。
そのため、1
で言及したとおりthis.$store.getters
をcomputedで利用し、return値に型を定義
することが望ましいです。
3. (this as any).hogeによるthisのanyマッピング
なぜ、泣く泣く(this as any).hoge
のようにthisをanyでマッピングするのかというとthisの参照先にundefindの可能性があるためでです。
import { TodoData } from '~/types/firestore'
~略~
computed: {
todos: TodoData[] {
return this.$store.getters['todo/all']
},
newlestTodo: {
// todosにundefindの可能性があればthisはtypeエラーとなる
return Math.max(...this.todos)
}
}
computedで値を定義するシチュエーションでは迷わず、nullの許可をしましょう。
また、this.todos
をが存在していたら処理を行うようにすると型エラーを免れ、型推論もなされます。
この策は、TypeScript 3.7のOptional chaining
を導入していない場合に有効です。
import { TodoData } from '~/types/firestore'
computed: {
// todosにnullの可能性を定義しておく
todos: TodoData[] | null {
return this.$store.getters['todo/all']
},
newlestTodo: string | null {
// this.todosnullの場合はnullを、存在していれば最新のtodoを取得する
return this.todos ? Math.max(...this.todos) : null
}
}
やったほうが良いこととやるべきこと
store編
storeの型定義はtypes
ディレクトリを作成し定義します。
以下の例はTodo
というstateで扱うデータ型を定義し、
- State
- Actions
- Getters
- Mutations
といったVuexのライフサイクルで使用する型を定義します。
ここで定義する型はあくまでもVuex内で使われる値の型推論が保証されるものです。
そのため、computedやdataでは別途
import firebase from 'firebase'
export interface Todo {
id: string
userId: string
todo: string
imageUrl: string
status: string
createdAt: firebase.firestore.Timestamp
updatedAt: firebase.firestore.Timestamp
}
export interface State {
all: Todo[]
}
export interface Getters {
all: Todo[]
}
export interface RootGetters {
'todo/all': Getters['all']
}
export interface Mutations {
setTodos: { todos: Todo[] }
}
export interface RootMutations {
'todo/setTodos': Mutations['setTodos']
}
export interface Actions {
fetchByUserId: { userId: string }
}
export interface RootActions {
'todo/fetchByUserId': Actions['fetchByUserId']
}
data編
dataを定義する際、与えられた値によって型が定義されますが、template
からの入力やmethod
によって値が書き換えられることも考慮して、interfaceを定義することができます。
例えばユーザー情報を入力する画面かつモーダルを実装する場合
interface LocalData {
name: string
address: {
zipcode: number | null // numberの初期値はnullにしておく
region: string
locality: stirng,
streetAddress: string,
extendedAddress: string
}
showModal: boolean
}
export default Vue.extend({
data: (): LocalData => ({
name: ''
address: {
zipcode: null
region: ''
locality: '',
streetAddress: '',
extendedAddress: ''
},
showModal: false
})
})
LocalDataような名前でそのコンポーネント
でのみ使用するinterfaceを定義出来ます。
もちろん、外部ファイルにて定義しているのであればそれをimportして定義することも出来ます。
computed編
上記のやったほうが良いこととやるべきこと
であげた対応法をご覧ください。
props編
親コンポーネントか子コンポーネントへ値を渡す際に使用するpropsでも型定義ができます。
親コンポーネントで定義した型と同じ型をここで定義することで型安全が保たれます。
props: {
todo: {
type: String,
default: ''
required: true
}
}
mixin編
mixinでVuexからtodosを取得し、各コンポーネントでtodosを扱いたい時は
const mixin = Vue.extend({
todos: TodoData[] {
return this.$store.getters['todo/all']
}
})
export default mixin
import todoMixin from '~/assets/todo_mixin'
export default Vue.extend({
mixins: [todoMixin]
})
のようにimportをしてあげるかと思います。
ただこのままだとtodoをtemplate内で使用はできますが、computedやmethodsではthisを使った参照で型エラーが発生します。
ここでも同様にtypesディレクトリ内に型定義ファイルを用意してあげます。
import Vue from 'vue'
import { TodoData } from '~/types/firestore'
declare module 'vue/types/vue' {
interface Vue {
todos: TodoData[]
}
}
これで型推論が効きます◎
終わりに
業務で扱うことでTypeScriptのありがたみや扱いづらさも感じていましたが、プロジェクトやプロダクトのグロースに向けて、品質の高いコードと治安を守るためにも以上のTIPSを使いながらコーディングをすると良いかもしれません。
途中にも登場した、Optional chainingやVue.js3.0でのTypeScript対応によってはよりよい体験が提供され、今回紹介したTIPSを必要としなくなるかもしれませんが2019年12月現在、TypeScript3.7をプロジェクトに導入していない場合は役立つと思います!
メンションつきツイートをしていただけるとたいへん喜びます!
thanks-mentionsというQiitaの記事を作者に対してメンションを飛ばしながらツイートが出来るPWAを作りました🚀
ぜひ、メンションつきツイートをしていただけるとたいへんとてもとても喜びます!
Vue #2 Advent Calendar 2019
明日、Vue #2 Advent Calendar 2019の9日目は、イイダリョウ @idr_zz
さんです!
※2020年1月更新
##MENTAでコードレビューやプログラミング勉強相談を行っています!
お気軽にメッセージを下さい!
プランはこちらになります!