THUMBS SHIFT→

このブログは主に親指シフトを用いて書かれています

一体僕達はどのようにしてimmutable.js+TypeScriptの環境を運用すれば良いのか

JSクソだと言い続けてはや幾月、僕です。

モダン開発環境おじさん「React+Redux+Typescript+immutable.js環境は良いぞ。」

とはよく言われていますが、immutable.jsとTypescriptを併用しようとするとかなりめんどくさくなることに、取り返しのつかなくなった時点で気づくことがあるというのはあまり知られていません。

素晴らしくモダンなimmutable.jsがTypescriptと相性悪いなんてあるわけないやろwwwと僕も最初は思っていました。

では以下のオブジェクトを見てみましょう。

const o = {
    name: 'arark',
    playerId: '0001',
    level: 23
}

これを型付けするのは簡単です。

interface user {
    name: string
    playerId: string
    rank: number
}

じゃあこれはどう型付けしましょう

import { Map } from "immutable";
const m = Map({
  name: "arark",
  playerId: "0001",
  level: 23
});

そう。。。めっちゃ面倒くさいのです。そもそも自分で定義しなくても型推論してくれるもんだと思ってたし。 immutable.jsでは以下のような問題が発生します。

import { Map, fromJS } from "immutable";

// JSのオブジェクトの場合
interface user {
  name: string;
  playerId: string;
  level: number;
}
const o: user = {
  name: "arark",
  playerId: "0001",
  level: 23
};
const name_mutable = o.name //stringと推論される(型付けしなくても)
const level_mutable = o.level // numberと推論される(型付けしなくても)
const none = o.none // エラーになる

// 普通のimmutable.Mapの場合
const u = Map({
  name: "arark",
  playerId: "0001",
  level: 23,
});

const name_immutable = u.get("name"); //string | numberになる。stringと推定してほしい
const hoge_immutable = u.get("hoge"); //string | numberになる。エラーにしてほしい。

このように明らかにJSのオブジェクトとは異なる動きをします。幾つか解決策は見つかっていて 一つは自分でgetなどのメソッドの型定義をオーバーライドすることです。*1*2

interface User {
  name: string
  address: string
  phoneNumber: number
}

interface ImmutableMap<T> extends Map<string, any> {
  get<K extends keyof T> (name: K): T[K];
}

type UserMap = ImmutableMap<User>

const user: User = {
  name: 'yuki',
  address: 'tokyo',
  phoneNumber: 8108012345678
}
const userMap: UserMap = Map(user)
const n = userMap.get('name') // stringと推論される
const age = userMap.get('age') // エラー

これは期待した通りに動き、問題も無いように思えます。しかし他にもある setIn, updataIn, mapなどのメソッドを使っていくたび新しく定義しなければいけないと考えるとかなり手間です。

もうひとつは

blog.mayflower.de

にあるように

import {Map, List} from 'immutable';

type AllowedValue =
  string |
  number |
  boolean |
  AllowedMap |
  AllowedList |
  TypedMap<any> |
  undefined;

interface AllowedList extends List<AllowedValue> {}

interface AllowedMap extends Map<string, AllowedValue> {}

export type MapTypeAllowedData<DataType> = {
  [K in keyof DataType]: AllowedValue;
};

export interface TypedMap<DataType extends MapTypeAllowedData<DataType>> extends Map<string, AllowedValue> {
  toJS(): DataType;
  get<K extends keyof DataType>(key: K, notSetValue?: DataType[K]): DataType[K];
  set<K extends keyof DataType>(key: K, value: DataType[K]): this;
}

const createTypedMap = <DataType extends MapTypeAllowedData<DataType>>(data: DataType): TypedMap<DataType> => Map(data) as any;

などと複雑な型定義をすることです。

「ファック!!!!!!!!!!俺は開発をしたいんであって型定義をしたいんじゃんねえええええええええ」

というのは至極まっとうな意見であって、実際僕もそう思います。ここまでくるともはやimmutable.jsが開発の邪魔になってくることさえ有ります。

そんな悩みに対する回答を、私はstackoverflowの奥地で見つけました。

stackoverflow.com

この一文に注目してみましょう

we converted to immer and we don't want to look back.(中略)We are happy.

そう、わざわざimmutable.jsなんて使いづらいものを使う必要はなかったということをこのコメントは気づかせてくれました。

このTypescriptとの不親和は本家リポジトリでもつとに指摘されていることではありますが、以下のissueに見られるように、なかなか改善される見込みはありません。 Immutable.js is essentially unmaintained · Issue #1689 · immutable-js/immutable-js · GitHub immer.jsに乗り換える人も多いようです。

僕?僕は使いません。もはや戻れないところまで来てしまったからさ......