最近TypescriptベースのVueで簡単なアプリを色々作ろうとしている。
あるアプリの作成を進めている中で、割と複雑な中身(関数や別のオブジェクトが含まれている)のオブジェクトを返す関数を実装することがあり、Missing return type on function.
の警告が頻発してしまった。
この警告をどう解消したらいいか、つまり返り値の型をどうやって指定すればいいのかすぐに思いつかなかったので、今回はその方法を考えてみた。
開発環境
- Vue 3.2.22
- Vue CLI 4.5.13
- typescript 4.1.5
- firebase 9.5.0
- eslint 6.7.2
ファイル構成と概要
例としてログインやログアウト機能を有する画面について考える。
VueのComposition APIではお馴染みのように、コンポーネント部分(views
ディレクトリ下)とロジック部分(composables
ディレクトリ下)を分割して別々のファイルで管理している。
├── src │ ├── composables │ │ ├── getUser.ts │ │ ├── useLogin.ts │ │ └── useLogout.ts │ ├── types │ │ └── ComporsableUtilTypes.ts │ └── views │ ├── Home.vue │ └── auth │ ├── Login.vue │ └── Signup.vue
例えば、ログイン機能のロジックを定義しているuseLogin.ts
は下記のようになっている。
import { ref } from 'vue' import { auth } from '@/firebase/config' const error = ref(null) const login = async (email: string, password: string) => { error.value = null await auth.signInWithEmailAndPassword(email, password) .then(() => { error.value = null }) .catch(err => { error.value = err.message }) } const useLogin = () => { return { login, error } } export default useLogin
処理の詳細については本題ではないので省略するが、Firebase Authenticationと連携してログインを実行するための非同期関数login
を定義し、useLogin
でerror
と一緒にlogin
を返している。
error
は処理中に何かエラーが発生した場合に表示するメッセージを格納するもので、内部値がstring
型のref
オブジェクトである。
ここで定義したuseLogin
をLogin.vue
コンポーネントでインポートし、login
やerror
を呼び出して使う想定である。
ここからが本題。この処理そのものは問題ないが、eslintを利用しているなどの場合、
Missing return type on function.
つまり「returnする値の型をちゃんと明示しろよ!」と警告される。
このように、関数やref
オブジェクトなどが入り混じっているオブジェクトが返り値となる時にどのように型を定義すればいいのか。
型定義の解決策
一番簡単な解決策(?)としては、所詮警告なので無視するか(): any => { }
とすることだろう。
しかし、それではTypescriptで開発を始めた意味があまりないと思われる。
そこで、返り値の型を自分で定義することにした。
ComporsableUtilTypes.ts
という型宣言用のファイルを作成し、useLogin
で返すオブジェクトの型をLoginUtilType
という名前で定義している。
import { Ref } from 'vue' export type LoginUtilType = { error: Ref<string | null>, login: (email: string, password: string) => Promise<void> }
login
関数の定義で注意が必要なのは、非同期関数なので返り値がPromise
となることである。(同期関数であれば単純に( … ) => void
などでいい。)
error
の中身はnull
も想定されるので、Union型を利用してRef<string | null>
と定義した。
これをuseLogin.ts
でインポートしてuseLogin
の返り値の型として指定する。
import { ref } from 'vue' import { auth } from '@/firebase/config' import { LoginUtilType } from '@/types/ComporsableUtilTypes' // 省略 const useLogin = (): LoginUtilType => { return { login, error } } export default useLogin
これで警告はなくなった! よさそう。
他の型定義も作ってみる
ログイン以外にもログアウトやユーザー情報取得のロジックがある場合はどうだろうか。
import { ref } from 'vue' import { auth } from '@/firebase/config' const error = ref(null) const logout = async () => { error.value = null await auth.signOut() .catch((err) => { error.value = err.message }) } const useLogout = () => { return { logout, error } } export default useLogout
import { ref } from 'vue' import { auth } from '@/firebase/config' const user = ref(auth.currentUser) auth.onAuthStateChanged(_user => { user.value = _user }) const getUser = () => { return { user } } export default getUser
この場合もuseLogout
やgetUser
の返り値の型を明示していなければ警告がでるので、LoginUtilType
と同様にLogoutUtilType
、GetUserUtilType
という型の定義をComporsableUtilTypes.ts
へ追加した。
import { Ref } from 'vue' import firebase from 'firebase/compat' type BaseUtilType = { error: Ref<string | null> } export type LoginUtilType = BaseUtilType & { login: (email: string, password: string) => Promise<void> } export type LogoutUtilType = BaseUtilType & { logout: () => Promise<void> } export type GetUserUtilType = { user: Ref<firebase.User | null> }
Ref<string | null>
の部分は共通になるのでBaseUtilType
として切り出し、
BaseUtilType & 別の型
のように連結した交差型(Intersection型)として定義した。
2箇所ぐらいであればわざわざここまでする必要はないかもしれないが、今後も同じような定義が増えた時に備えている。
GetUserUtilType
の定義については、Firebase Authenticationのユーザー情報オブジェクトfirebase.User
を指定している部分が他と異なる以外は基本的に変わらない。
これらもそれぞれコンポーネント側でインポートして使うことで警告はなくなった!
他にもboolean
型のref
オブジェクトも返したいとかいう場合は、BaseUtilType
をRef<string | boolean | null>
などと修正してやればいいだろう。
まとめ
Typescriptの導入目的は、型を明確に定義して堅牢性の高いコードを実現することなので、今回のように型チェックの警告が出ているから後付けで型を定義しようという考え方は本末転倒な気もするが、とりあえず解決したようなのでよしとする。
これ以外にもっとスマートで合理的な方法があるかもしれない。
これを機にもう少しTypescriptの型の定義に関してちゃんと理解を深めよう。
参考