はじめに
業務でVue3 × Pinia × TypeScriptのプロジェクトを担当しました。
単一コンポーネントファイル上での型付けの方法はcomposition apiが導入されてしばらく経っているので情報も多く書きやすかったです。
Piniaの使い方で参考になる日本語の情報が少なく苦労しました。
今回はPiniaを使用した時の型定義の方法を記録として残します。これからPiniaを導入する方の参考になればと思います。
単一コンポーネントでの型付けの方法をまとめた記事も投稿していますので参考にしてください
https://qiita.com/manzoku_bukuro/items/a2dd20e787617b033592
Storeの記述法
型付けをする前にPiniaのStoreファイルの記述法を確認しておきます。PiniaのStoreファイルの記述方法ですが、Composition API風の書き方とOptions API風の書き方があります。
Composition API風の記述法
以下のような記述法になります。
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
})
- stateはComposition APIのref,reactiveを使用して定義
- gettersはcomputedを使用して定義
- actionsは関数を利用して定義
- 定義したstate,getters,actionsの内容を最後にreturnで返却する
記述法の特徴としてはこんな感じです。state,getters,actionsというプロパティを記載しなくてよいので、スッキリとした書き方が出来る印象です。
ですが所感としてはスッキリしすぎて、state,getters,actionsの区別がつきにくい...(特にgettersとactions)
そして後ほど記述するDefineStoreOptions
を利用した型定義の方法を使用するために、私の参加したプロジェクトではOptions API風の書き方で記述しました。
Options API風の記述法
import { defineStore } from 'pinia';
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
counter: 1,
}),
getters: {
doubleCount: (state) => return state.counter * 2;
},
actions: {
const increment = () => {
count.value++;
};
},
})
上記のソースをOptions API風に書き換えました。idを定義する部分以外はVuexと書き方は一緒なので、こちらの方が分かりやすい人も多いと思います。Piniaの公式ドキュメントを見ても基本はOptions API風の記載がされている気がする。
以降はOptions API風の記述法で型定義の方法を記述していきます。
Piniaでの型定義の方法
Storeで型定義する方法はいくつかあるので私が知る限りの方法を記述します。
以下の型定義していないstoreディレクトリ配下のファイルの型付をしていきます。
初期状態のstoreファイル
import { defineStore } from "pinia";
export const useExampleStore = defineStore({
id: 'example',
state: () => ({
name: '田中太郎',
sales: 0,
menus: ['ホーム', '一覧'],
book: { title: '人間失格', price: 1070 },
}),
getters: {
introduction: (state) => {
return `私の名前は${state.name}です。`;
},
bookPrice: (state) => {
return state.book.price * 1.08;
},
},
actions: {
changeName(name) {
this.name = name;
},
buyBook(count) {
this.sales = this.bookPrice * count;
return `現在の売上は${this.sales}です。`
}
},
})
パターン1. Stateの変数を一つずつ型定義する
型アサーションas
を利用する
Stete内の個々の変数の後ろにas
を利用して定義するパターンです。
import { defineStore } from "pinia";
export interface Book {
title: string;
price: number;
}
export const useExampleStore = defineStore({
id: 'example',
state: () => ({
name: '田中太郎' as string,
sales: 0 as number,
menus: ['ホーム', '一覧'] as string[],
book: { title: '人間失格', price: 1070 } as Book,
}),
...
})
パターン2. Stateの変数をまとめて型定義する
Stateプロパティの変数が定義されたオブジェクトをまとめてくるんでas
で定義する
変数の個々の後ろで型定義するのでは無く、stateプロパティを包んでまとめて型定義する方法です。
state: () => ({
name: '田中太郎',
sales: 0,
menus: ['ホーム', '一覧'],
book: { title: '人間失格', price: 1070 },
} as {
name: string,
sales: number,
menus: string[],
book: Book,
}),
これでもいいのですが、stateで定義した型まとめてinterfaceで包んでしまった方が分かりやすいと思います。
...
export interface exampleState {
name: string;
sales: number;
menus: string[];
book: Book;
}
export const useExampleStore = defineStore({
id: 'example',
state: () => ({
name: '田中太郎',
sales: 0,
menus: ['ホーム', '一覧'],
book: { title: '人間失格', price: 1070 },
} as exampleState),
)}
Stateの()
の後ろで型定義したオブジェクトを渡す
以下のようにstateの後ろに型定義のオブジェクトを渡すやり方です。以下の場合はオブジェクトをinterfaceで包んで渡しています。
export interface exampleState {
name: string;
sales: number;
menus: string[];
book: Book;
}
export const useExampleStore = defineStore({
id: 'example',
state: (): exampleState => ({
name: '田中太郎',
sales: 0,
menus: ['ホーム', '一覧'],
book: { title: '人間失格', price: 1070 },
}),
)}
(重要!!)パターン3. DefineStoreOptionsを使用してState,Getters,Actionsを型定義する
この記事で最も有益な情報とも言える型定義の方法です。
defineStore()のオプション引数 (optional parameter)という機能を使います。defineStoreの頭でId,State,Gtters,Actionsの型定義したパラメータを渡すことでdefineStore()の内部では型定義を省略することが出来ます。
ネットで検索してもDefineStoreOptionsで型定義している情報は少なく、Pinia公式ドキュメントを和訳して参考し実装しました。以下ソースになります。
import { defineStore, type _GettersTree } from "pinia";
export interface Book {
title: string;
price: number;
}
export interface exampleState {
name: string;
sales: number;
menus: string[];
book: Book;
}
export interface exampleGetters extends _GettersTree<exampleState> {
introduction: (state: exampleState) => string;
bookPrice: (state: exampleState) => number;
}
export interface exampleActions {
changeName: (name: string) => void;
buyBook: (count: number) => string;
}
export const useSampleStore = defineStore<string, exampleState, exampleGetters, exampleActions>({
id: 'example',
state: () => ({
name: '田中太郎',
sales: 0,
menus: ['ホーム', '一覧'],
book: { title: '人間失格', price: 1070 },
}),
getters: {
introduction: (state) => {
return `私の名前は${state.name}です。`;
},
bookPrice: (state) => {
return state.book.price * 1.08;
},
},
actions: {
changeName(name) {
this.name = name;
},
buyBook(count) {
this.sales = this.bookPrice * count;
return `現在の売上は${this.sales}です。`
}
},
})
defineStore<Id, S, G, A>
の形式で型を渡す
exampleState, exampleGetters, exampleActionsをそれぞれintefaceで型定義し、defineStore<string(idの型), exampleState(stateの型), exampleGetters(gettersの型), exampleActions(actionsの型)>
のようにdefineStoreの頭で渡しています。こうすることで、defineStoreの内部では型定義に関する記述は不要になりソースコードがスッキリとします。
_GettersTree
を使う
exampleGetters(gettersの型)
を定義した際、最初は以下のように記載しました。
export interface exampleGetters {
introduction: (state: exampleState) => string
bookPrice: (state: exampleState) => number
}
しかしこのままでは以下のようなエラー文が表示されてしまいました。
うーん、どうやらgettersプロパティの引数のstateの型の付け方に問題がありそう。
gettersで定義されているプロパティの引数ではstateが定義されています。こちらのstateにも型をつけないといけないのですが、少し特殊な記述法をする必要がありました。
export interface exampleGetters extends _GettersTree<exampleState> {
introduction: (state: exampleState) => string;
bookPrice: (state: exampleState) => number;
}
以上のように_GettersTree<Stateの型>
という形で定義したStateの型を継承する必要があります。これでエラーは解消されました。
使用しないプロパティがあれば空のオブジェクトを渡す
gettersやactionsが未定義の時にDefineStoreOptionsを使用したい場合は、使用しないプロパティの引数に空のオブジェクトを渡せば良いです。
export const useSampleStore = defineStore<string, {}, {}, {}>({
id: 'example',
state: () => ({}),
getters: {},
actions: {},
})
おわりに
実際にプロジェクトでメインで使用したPiniaの型定義の方法は、パターン3のDefineStoreOptionsを使用した方法となります。内部にごちゃごちゃと型付の記述をする必要が無く、どんな処理をしているか分かりやすいソースコードになりました。
しかしState定義する変数が一つの場合や、GettersもActionsも定義されていないStoreファイルを扱う場合はDefineStoreOptionsを使用すると無駄な部分も目立つ為、パターン1やパターン2の方法で型定義することもありました。
必要に応じて臨機応変に使用していただければと思います。
Twitterもやってます。