Open5

Remedaのすすめ

じょうげんじょうげん

TypeScript, JavaScriptのユーティリティ集ライブラリのremedaについてまとめます。
この手のライブラリとしてはlodashが有名ですが、remedaはTypeScriptでの開発がされ、ツリーシェイクを意識した実装がなされておりよりモダンな実装となっています。
lodashに似た使い方となっているため、使用したことがある方であればすぐに理解できるAPIとなっています。

import * as _ from "lodash"

const users = [{ id: 1, name: 'Dave' }, { id: 2, name: 'Eve' }];
console.log(_.map(users, 'name')); // ['Dave', 'Eve']
import * as R from "remeda"
const users = [{ id: 1, name: 'Dave' }, { id: 2, name: 'Eve' }];
console.log(R.map(users, 'name')); // ['Dave', 'Eve']

このように、全く同じ使い方の関数も多いです。
importの名前は自分でつけられるのでRじゃなく_にすることもできますし、namespace無しで直接呼ぶこともできます。

namespace無しだと他のライブラリの同名の関数を使用したときにどちらのものなのか区別しづらくなってしまうため、明示的にするためにもnamespaceありで呼ぶのが個人的にはおすすめです。

一番便利なポイントは、Data FirstとData Last双方の実装がオーバーロードで実装されている点です。
lodashではこの使い分けをしたい場合、lodash/fpからインポートしなければなりません。

import { flow, filter, map } from "lodash/fp";

const users = [
  { id: 1, name: "Dave", email: "dave@example.com", isActive: true },
  { id: 2, name: "Eve", email: "eve@example.com", isActive: false },
  { id: 3, name: "Alice", email: "alice@example.com", isActive: true },
];

// `flow` を使ってパイプライン処理
const getActiveUserEmails = flow(
  filter({ isActive: true }), // アクティブユーザーをフィルタリング
  map("email") // メールアドレスのみ取得
);

console.log(getActiveUserEmails(users)); // ['dave@example.com', 'alice@example.com']

remedaでは以下のように、単一のエントリポイントからインポートし、実行時に渡される引数によって通常の関数としての呼び出しなのか、カリー化された関数を返すべきなのかが判断されるため、使用者はその違いを意識することなくコードの記述に専念できます。

import { pipe, filter, map } from "remeda";

const users = [
  { id: 1, name: "Dave", email: "dave@example.com", isActive: true },
  { id: 2, name: "Eve", email: "eve@example.com", isActive: false },
  { id: 3, name: "Alice", email: "alice@example.com", isActive: true },
];

// `pipe` を使ったデータ処理
const activeUserEmails = pipe(
  users,
  filter(user => user.isActive), // アクティブユーザーをフィルタリング
  map(user => user.email) // メールアドレスのみ取得
);

console.log(activeUserEmails); // ['dave@example.com', 'alice@example.com']
じょうげんじょうげん

外部ライブラリに依存することについて

この手のライブラリは類似のライブラリが多く、どれを採用するかや、そもそも採用せずに標準で頑張るという選択肢もあり、議論のネタとなることも多いです。
この点についての詳しい判断は他の記事に譲りますが、私としては入れるメリットの方が勝つと考えています。
可読性が高まる点と、処理を自作、メンテナンスしなくて良い点から、工数が節約できるためです。
ただし、それと決めたら可能な限りそのユーティリティを使用すべきです。
場所によって使う使わないがぶれているようでは後から入った人が迷う原因となり、可読性が悪くなるためです。

じょうげんじょうげん

ケース毎のよく使う使い方

  • 配列操作と遅延評価
    配列操作したいだけならArray.prototypeあるのに何に使うんだ?
    私が初めてこの系統のライブラリを見たときに思ったのがこの疑問です。
    この答えが遅延評価です!
    配列操作はその関数呼び出しでその都度配列の操作をメモリ上に保持しながら計算を行います。
    そのため、メソッドチェーンで繋げば繋ぐほどメモリ消費量が上がり、何度も計算処理を行うことになります。
    通常の関数の知識だけでこれを避けようとすると、reduceを使うことになりますが、型が効かなかったり、複数の処理を一つの関数内にまとめて書くことになり可読性が悪くなります。

JavaScriptの世界では自分でGenerator関数を書けば遅延評価を実現できますが、remedaを使うとGeneratorを学習せずとも遅延評価される配列操作を実現できます。

  • 制御フロー
    どちらかというと、積極的に使うよりかはremedaのpipeの中で条件分岐をしたくなったときに文法をそろえて書けるから使用するという消極的な利用になります。
    TODO

  • 即時実行関数
    TODO

  • 標準ライブラリ以上に型の効いた処理を行いたい

TypeScriptは型のないJavaScriptに型を後付けで当てはめた言語のため、ものによっては自分自身で型を保証しなければならない場面が出てきます。
そのような場面でasを使う際は、絶対に間違いでないことを確認しなければなりません。
ですが、実はremedaを使えば解決する場面は多いです。
remedaが内部でこの処理だったらこの型になるということを保証してasを使ってくれているので間違いない型推論を保つことができます。

  • 型安全にObjectを扱ったループ処理を行う
    TODO

  • スプレッド構文による取り出しを安全にする

スプレッド構文は便利ですが、乱用すると間違ったプロパティを渡してしまいバグを生み出してしまったり、下手すると情報漏洩の原因になります。
pickとomitを使えば、それぞれ必要なパラメータだけを引数や戻り値に取り出してセットすることができます。

  • 指定回数ループする

同じ関数を何度も呼び出して配列を作りたいときどのように実装していますか?
JavaScriptでは組み込みの方法で指定回数ループする方法がなく、Array.from({length:n})Array(n).fill(0)などの配列を作ってforで回す直感的でない方法でしかループを行うことができません。
さもなくば不要な中間変数をletで宣言させられます。for (let i;i==n;i++)
Remedaのtimesを使えばこの通り。

じょうげんじょうげん

R.pick
ランタイム上は存在しないキーを指定したとしてもエラーにならないが、型上で存在しないキーが含まれるとエラーになってしまう。
これを改善したい。

type Point2D = {
  kind: '2d';
  x: number;
  y: number;
};

type Point3D = {
  kind: '3d';
  x: number;
  y: number;
  z: number;
};

type Point = Point2D | Point3D;

declare const point: Point;

const data = R.pick(data, [
"x" // <- OK
"z" // <- Error
 ])
じょうげんじょうげん
type KeyOfUnion<T> = T extends T ? keyof T : never;

type DistributivePick<T, K extends KeyOfUnion<T>> = T extends T
  ? Pick<T, Extract<K, keyof T>>
  : never;

declare function pick<
  T,
  K extends ReadonlyArray<KeyOfUnion<T>>
>(
  obj: T,
  keys: K
): DistributivePick<T, K[number]>;