yuki8888nmの日記

Web系エンジニアのブログ

Immutable×TypeScriptでMapのgetとsetをGenericsで型定義する

初投稿です。

TypeScript × Immutable.jsのMapについての記事になります。

 

会社でReactやImmutableは使用しているものの、TypeScriptは導入しておらず、また今まで自分が静的型付けを行う言語に触れてこなかったということもあり先月ぐらいから勉強しています。

その中でImmutableのMapのメソッドについてはMapのinitializeに使用するobjectのプロパティの値の型に基づいた型チェックをしてほしかったのですが、用意されている型定義ファイルではそれができずGenericsを使ってメソッドを定義してあげる必要があったのでそのあたりのシェアです。
※Immutable.jsの基本的な書き方などの説明はありません。

 
上記についての説明をします、以下のようなコードがあったときに、
 

import * as I from 'immutable'


const user = {

    name: 'yuki',

    address: 'tokyo',

    phoneNumber: 08012345678

 }

const userMap = I.Map(user)

userMap.get('age') //ここでエラーが起こってほしい

userMap.get('age')でuserの存在しないキー名を指定しているので型チェックで怒ってほしい、しかしもともと用意されている型定義ファイルによってMapの型定義はMap<キーの型, 値の型>という書き方のため、

const userMap: Map<string, any> = I.Map(user)
userMap.get('name') //OK
userMap.get(1) //Error
userMap.get('age') //OK ←キー名に基づく型チェックができない

 
というように元になったobjectのキー名を意識せず、TypeScriptによる型チェックエラーにもならずにコンパイルが通ってしまいます。

コードの大部分をImmutableにしているため、せめてgetとsetぐらいはちゃんと使用するobjectに基づいた型チェックしてほしいと思い、Mapのinitializeに使用するobjectの型に基づいたMapの型定義ができないかと調べた結果、Genericsで対応できることがわかりました。

参考:
stackoverflow.comstackoverflow.com


結論から。以下のように書くことでgetの型チェックが可能になります。

import * as I from 'immutable'

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

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

type UserMap = ImmutableMap<User>

const user: User = {
    name: 'yuki',
    address: 'tokyo'
    phoneNumber: 08012345678
}
const userMap: UserMap = I.Map(user)
userMap.get('age') //[ts] Argment of type'"age"' is not assignable to parameter of type "name" | "address" | "phoneNumber"

 
まず、一番上のinterface UserでUser objectの型定義をし、その次にImmutableMapのところでGenericsを使ってMapのgetを定義し、そのinterfaceを使用してtype UserMapを定義しています。

export interface ImmutableMap<T> extends I.Map<string, any> {
    get<K extends of T> (name: K): T[K];
}
type UserMap = ImmutableMap<User>

 
Tはinterface ImmutableMapの引数を指定するのでtype UserMapでは、
 

type UserMap = ImmutableMap<User>

 
のように< (interface) User >の部分に当たり、type UserMapと型定義されたものはTをinterface Userとしたgetを持つということが定義されます。
次にそのgetを分解します、TypeScriptのGenericsの書き方として

get<K>(name: K)

 
と書くことで、getの第一引数nameの型Kに型定義することができます。
その< K >はここでは
 

<K extends keyof T>

となっています。keyofはTのプロパティ名の直和型(Union Type)を返し、Kはそれをextendsしています。Tはinterface Userに当たるので上記は、
 

<K extends 'name'|'address'|'phoneNumber'>

 
と同義です。そして最後のT[K]で戻り値にT objectのキーKに当たる型を指定しています。
 
以上からtype UserMapのgetの引数にはinterface Userのキーしかとることができなくなり、その戻り値はUserのキーに対応する値の型になることが保証されます。
 
上記を理解した後、setは自分で書いてみました。

 
//中略
 
set<K exntends keyof T> (name: K, value: T[K]): ImmutableMap<T>
 
//中略
 
user.set('name', 'hogehoge') //OK
user.set('name', 000) //Error
user.set('age', 20) //Error

 
いい感じです。
 
とりあえず上記のような形でImmutableのMapのgetとsetをGenericsを使用して定義してみました。Immutable × TypeScriptの環境ではMapで最も使用する上記2つを書いておくだけでも大分効果はあると思います。
 
Generics初学習だったのですがTypeScriptを使用するならある程度学んでおいたほうがいいなと感じました、あと純粋に面白かったので今年は引き続きGenericsと仲良くなっていこうと思います。個人的にはImmutableのMapに関してはgetIn、setIn、mergeぐらいまでは型チェックを行ってほしいので、深追いはしませんが上記のようにいいかんじに定義できたらいいなぁと思っています。
 
 間違ってるところ等ございましたらご指摘お願いしますm(_ _)m