typealiasからinline classに置き換えて代入互換性をなくす

inline class を知った

inline class UserId(val value: String)

Kotlin 1.4.30 のリリース記事を読んで、inline classを知ったのでドキュメントを読んでみました。

個人的にかなり有用に感じたのは、typealiasと違ってassignment-compatible(代入互換性)がない点です。

個人開発アプリでtypealiasを使っているところがあるので、そこを置き換えつつ、その有用性を体感してもらえたらと思います。

実行環境

  • Kotlin バージョン 1.4.30

修正前のコード

それぞれtypealiasRoomIdUserIdを定義しています。 どちらもString型であることには変わりませんが、data classなどに列挙したときに役割がパッと見やすくなります。

しかしパッと見やすくなるだけで、Value Object として機能していないので注意です。

typealias RoomId = String
typealias UserId = String

data class Room(
    val id: RoomId,
    val ownerId: UserId,
    ...
)

typealiasには代入互換性がある

以下のように、RoomRepositoryを実装しました。 roomIdを引数にとって、指定したRoomのデータを取得するような実装のつもりです。

interface RoomRepository {

    suspend fun get(roomId: RoomId): Room
}

以下は、呼び出し元の実装例です。

実装例①

問題なく動作します。

fun getRoom(roomId: RoomId, userId: UserId) {
    viewModelScope.launch {
        val room = roomRepository.get(roomId)
    }
}

実装例②

実装例①と違ってroomRepository.get(...)の引数にuserIdを渡していますが、問題なく動作します(!?)。

roomIduserIdは別の型であるかのように見えますが、実際はどちらもString型なので代入ができてしまいます。 このままだと意図しない代入が起きてしまいます。

fun getRoom(roomId: RoomId, userId: UserId) {
    viewModelScope.launch {
        val room = roomRepository.get(userId)
    }
}

修正後のコード

inline classUserIdRoomIdを作成します。 UserIdRoomIdの型を使っているところに変更はありません。 これによって、実装例②のコードはエラーになります。

inline class UserId(val value: String)
inline class RoomId(val value: String)

data class Room(
    val id: RoomId,
    val ownerId: UserId,
    ...
)

まとめ

typealiasは代入互換性があるので、意図しない代入ができてしまいます。

一方で、inline classは代入互換性がありません。これによって意図しない代入を防ぐことができ、Value Object として扱うことができます。

typealiasのままでも注意していればよくない?」という方もいるかと思いますが、言語の仕組みを利用してヒューマンエラーが起こらないようにした方がより不具合がない開発ができると思います。