TypeScriptで指定したfieldの値をキーとしたオブジェクトを作る
こんにちは。
珍しく1週間ただずの更新です。
最近はTypeScriptを結構書いていて、ちょっと知見もたまったのと、仕事が一段落してちょっと時間ができたので、今日はTypeScriptについて書きます。
指定したfieldの値をキーとしたオブジェクトを作る
普段の業務ではMongoDBを使っていたりする関係で、2つのコレクションから取ってきた複数のドキュメントをコードで、特定のfiledをキーにしてジョインしたいということが、たまにあります。
const books = [ {author_id: 1, title: 'Coloring for beginners'}, {author_id: 1, title: 'Advanced coloring'}, {author_id: 2, title: '50 Hikes in New England'}, {author_id: 2, title: '50 Hikes in Illinois'}, {author_id: 3, title: 'String Theory for Dummies'}, {author_id: 5, title: 'Map-Reduce for Fun and Profit'} ]; const authors = [ {id: 1, name: 'adam'}, {id: 2, name: 'bob'}, {id: 3, name: 'charlie'}, {id: 4, name: 'diane'} ]; // ジョインしたい cosnt joined = [ {author_id: 1, title: "Coloring for beginners", name: "adam"}, {author_id: 1, title: "Advanced coloring", name: "adam"}, {author_id: 2, title: "50 Hikes in New England", name: "bob"}, {author_id: 2, title: "50 Hikes in Illinois", name: "bob"}, {author_id: 3, title: "String Theory for Dummies", name: "charlie"}, {author_id: 5, title: "Map-Reduce for Fun and Profit", name: undefined}, ]
ジョインするアプローチも色々あると思いますが、普段Node.jsでは、ジョインしたいドキュメントの配列から特定のフィールドの値をキーとしたオブジェクトを作成して、もう一方のドキュメントの配列をmapで回してジョインしていく、という方法を使ってました。
// { "1": {id: 1, name: 'adam'}, "2": {id: 2, name: 'bob'}, ...} のようなオブジェクトを作る const mapedObject = authors.reduce((ret, v) => { ret[v.id] = v; return ret; }, {}); const res = books.map(b => { // bookのauthor_idを使って、紐づいているauthorを取得 const a = mapedObject[b['author_id']]; return { ...b, name: a ? a.name : undefined // left join的な感じ } })
特定のキーでmapedObjectを作るのは、色々なところで使えるので
const toMap = (arr, key) => { return arr.reduce((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
という感じで関数化してます。
今日はこれをTypeScript化するときに考えたことのまとめです。
さっきのNode.jsのコードをそのままTypeScript化しようとすると
const toMap = <T>(arr: T[], key: string):{[k: string]: T} => { return arr.reduce<{ [k: string]: T }>((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
こんな感じにできそうですが、以下のようなエラーがでてコンパルが通りません。
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'. No index signature with a parameter of type 'string' was found on type 'unknown'.
v[key]
で T
から値を取り出そうとしますが、 Tのindex signatureが宣言されていないので unknown型
になっており、v[key]
のように直接的に値をとることができないためです。
この問題は、Tのindex signatureを宣言することで解決できます。
const toMap = <T extends {[k: string]: any}>(list: T[], key: string):{[k: string]: T} => { return list.reduce<{ [k: string]: T }>((ret, v) => { const a = v[key]; ret[a] = v; return ret; }, {}); };
T extends {[k: string]: any}
とすることでTのindex signatureをstringと宣言したのでコンパイルは通るようになりました。
しかし、keyがただのstringだとTが持っていないキーを指定できてしまうのがイケてないので、keyof を使ってもうちょっと工夫してみます。
const toMap = <T extends {[k: string]: any}>(arr: T[], key: keyof T):{[k: string]: T} => { return arr.reduce<{ [k: string]: T }>((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
第2引数の型を keyof T
とすることで、Tが持つものしか渡せなくなります。
また、こうすることでエディタもサジェストしてくれるようになります。
Map Objectを使う
上記の方法でもいいと思いますが、Map Object版を考えて見ました。
const toMap = <T, K extends keyof T>( arr: T[], key: K ): Map<T[K], T> => { return arr.map<[T[K], T]>(e => [e[key], e]) // ['キーにしたい値', T] のtupleを作る .reduce<Map<T[K], T>>((acc, [k, v]) => { // Map Objectを作成 & set して returnする。 acc.set(k, v); return acc; }, new Map<T[K], T>()); };
Map Objectを使うと、index signatureを気にしなくて済みます。
また、Map Objectの方が entries()
, keys()
, values()
などのメソッドがあり、個人的にはobjectよりも色々扱いやすいのでMap Objectを使うのにハマってます。
まとめ
もうちょっと、色々なtipsを書こうと考えていましたが、書いてみると分量が多かったので今日はこれだけにします。
また時間があれば、ブログにまとめてみます。
最後までご拝読ありがとうございました。