LoginSignup
0
0

Mongooseのモデルをホットリロードすると上書きできなくてエラーで死ぬので、モデルが存在していたら上書きしないようにするが、雑にやると型がつかないしきっちりやると面倒なのをどうにかする

Last updated at Posted at 2023-10-25

タイトルを参照のこと

言いたいことはタイトルに詰めてある。
長いタイトルですまない。

Mongooseで型定義する。ミニマムだとこんな感じ

import { model } from 'mongoose'
const USER = 'user'
const UserSchema = new Schema({
  name:String
})
export const UserModel = model(USER, UserSchema)

これで良いのだが、開発中にホットリロードをさせると、以下のエラーで死ぬ。

 Cannot overwrite model once compiled Mongoose

model()を呼び出す際に同じ名前で登録してはいかんと。

ググるとこういう解法がいくつかヒットする。

- import { model } from 'mongoose'
+ import { model, models } from 'mongoose'
- export const UserModel = model(USER, UserSchema)
+ export const UserModel = models[USER] || model(USER, UserSchema)

定義したモデルはmodels[model_name]にあるので、すでに定義されていたら(ホットリロード時)それを利用すればよいということだ。
(上書きしてないのに更新したモデルが反映される仕組みは正直よくわからない。まぁコネクションのバッファリングのようにうまいことやってんでしょう)
(上書きしていないので当然モデルの更新自体は反映されない。モデルが更新される場合は大人しく再起動しよう)

これで確かに動作するのだが、この場合のmodels[USER]だと戻る型が汎用のモデルとなり、利用する側で具体的な型情報が使えない。これでは何のためにtsを書いているのかわからない。

更にググると、これを解消するのに、スキーマを以下のように定義する方法が出てくる。

import { model, models, Model } from 'mongoose'
const USER = 'user'
const UserSchema = new Schema({
  name:String
})
type TUser = {
  name:string// < ????
}
interface IUser extends Model<TUser> {}
export const UserModel = models[USER] as IUser || model(USER, UserSchema)

さっきまでスキーマの中にしかなかったnameが二重管理になってしまう。他のフィールドやメソッドを増やすたび同期させるのは地獄だし、なによりホットリロードのためにこんなコードが増えるというのは耐え難い。
これは公式にも載ってる型定義の方法でクールだみたいな反応があったが正気なのか。
DRY原則ってもうおじさんしか知らないの?

こんな解法もあった

function getModel(){
  return model(USER, UserSchema)
}
export const UserModel = (models[USER] || getModel()) as ReturnType<typeof getModel>

これはなるほど賢い。やはりホットリロードのためにほぼ無意味な関数を作るところに若干引っかかるものの、まぁ許容範囲だろう。
この"引数によって戻りの型が変わる関数を実際には実行せずに結果の型だけ得たい"時、関数でラップしてReturnTypeを使うというのはtsのパターンの一つであろう。覚えておいて損はない。
これでいいかと妥協しかけたその時天啓が降りてきた。
こうすりゃいいんじゃないか。

const _UserModel = models[USER] as unknown as null
export const UserModel = _UserModel ?? model(USER, UserSchema)

modelsにストアされている定義は常にnullであり、(推論としては)常にmodel()の実行がされるのだと型システムを騙すことで、返される型情報としてはmodel()実行時のものだけとなる。
もちろん実際のホットリロードの際にはmodelsにある定義が返り、model()の実行はされない。
ランタイムの動作と型情報に齟齬が起きるのはいかがなのかという指摘はあろうが、そういう型システムをハックしている感も含めて極めてエレガントだと思っている。HRのための型のための型など書きたくないのだ。
それをさらにゴルフしてこうだ。

export const UserModel = models[USER] as unknown as null ?? model(USER, UserSchema)

調べた限りではこの方法は見当たらなかったので特許を取りたいくらいだ。
このパターンに名前がついていたら教えてほしい。

なお元も子もないがこれでもいいというのも見た。
ライブラリの用意しているオブジェクトを壊すのは動作に不安があるのと、行儀としても抵抗があったので採用していない。

delete models[USER]
export const UserModel = model(USER, UserSchema)

というか

StackOverflowのトピックは立てられたのが10年前で、最近まで投稿されている。
本来差し替えられるような方法用意してくれるべきよな。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0