HAKUTAI Tech Notes

IT関連、数学のことなどを主として思いつきで色々書き溜めていきます

Typescriptで中身が複雑なオブジェクトの型を定義する

最近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を定義し、useLoginerrorと一緒にloginを返している。
errorは処理中に何かエラーが発生した場合に表示するメッセージを格納するもので、内部値がstring型のrefオブジェクトである。
ここで定義したuseLoginLogin.vueコンポーネントでインポートし、loginerrorを呼び出して使う想定である。

ここからが本題。この処理そのものは問題ないが、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

この場合もuseLogoutgetUserの返り値の型を明示していなければ警告がでるので、LoginUtilTypeと同様にLogoutUtilTypeGetUserUtilTypeという型の定義を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オブジェクトも返したいとかいう場合は、BaseUtilTypeRef<string | boolean | null>などと修正してやればいいだろう。

まとめ

Typescriptの導入目的は、型を明確に定義して堅牢性の高いコードを実現することなので、今回のように型チェックの警告が出ているから後付けで型を定義しようという考え方は本末転倒な気もするが、とりあえず解決したようなのでよしとする。
これ以外にもっとスマートで合理的な方法があるかもしれない。
これを機にもう少しTypescriptの型の定義に関してちゃんと理解を深めよう。


参考