🎄

実例 mapOrElse() / TypeScript一人カレンダー

2022/12/24に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の22日目です。昨日は『if文とswitch文は使い分ける?』を紹介しました。

ジェネリクスはどういうときに使うか

ここまで、アドベントカレンダーではたくさんのUtility Typesを紹介してきました。Mapped TypesConditional Typesを組み合わせて、Assertion Functionsも組み合わせて、いろんなことができる。そうやってTypeScriptに対する知識を付けていくなかで、もしジェネリクスを扱うならばあれもこれも組み合わせる必要があると受け止めてしまっていれば、それは誤解です。

ジェネリクスというのは、もっと日常的なプログラミングの中でカジュアルに採用してよいです。本日は筆者が業務で使っているユーティリティ関数mapOrElse()を紹介します。

mapOrElse()

次の関数mapOrElse()と、その内部実装convertOrElse()は、筆者がGraphQLを扱う案件にて採用していたものです。

function convertOrElse<T extends readonly unknown[], U, V>(
  arr: T | null | undefined,
  fn: (v: T) => U,
  orElse: V
): U | V {
  return Array.isArray(arr) ? fn(arr) : orElse;
}

function mapOrElse<T, U, V = U[]>(
  arr: T[] | readonly T[] | null | undefined,
  fn: (v: T) => U,
  orElse?: V
): U[] | V {
  return convertOrElse(arr, (v) => v.map(fn), orElse ?? ([] as U[]));
}

筆者が過去に参加した案件では、GraphQLサーバーからレスポンスを扱って配列処理を実施したいときに、そのサーバーはlength 1以上であれば配列を返すかわりに、length 0であれば空配列ではなくundefinedを返す挙動に出会いました。この辺は相手方の実装次第なのでなんとも言い難い部分があるのですが、毎回undefinedチェックを必要としてしまい、可読性も開発体験も落ちている状況でした。

そんな状況を見かねて、毎度書かれるundefinedチェックを抽象化する目的でこの関数を実装しました。この関数は型パラメータとしてT, U, Vの3つを取りますが、これまでに散々紹介してきたUtility Types自体は使っていません。関数の引数を抽象化する目的のみに型パラメータを扱っています。

ややこしそうに見えますが「T型の配列をArray.prototype.map()U型の配列にして返すか、あるいは初期値Vを返す。初期値を指定しないのであれば、U型の空配列を初期値として扱う」という内容です。このように型パラメータを伴う関数を、筆者は特別感なく普通に書いています。

身近なジェネリクスを扱う例

ジェネリクス (Generics) というだけで身構えてしまうTypeScript利用者を、筆者は一人や二人ではない数で見てきました。<T>が出てくるだけで「もうわからない」としてしまうのです。

このアドベントカレンダーも22日目であるため、1日目からの読者がここへきてまだわからないということはないと期待したいですが、ジェネリクスへの抵抗感をどうやって取り除けばよいかというのは筆者の業務、特にコンサルタント、メンタリング、講演といった方面の業務でも常に課題でした。

ジェネリクスはなんら特別でも高度でもなく開発現場の身近に溢れています。例えば、我々が配列を扱えているのは配列型がArray<T>と定義されているおかげです。

https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/lib/lib.es5.d.ts#L1300

Promiseの値を返す関数の先頭にawaitとつけるだけでレスポンス値が取得できる、これもPromise<T>として定義されているおかげです。

https://github.com/microsoft/TypeScript/blob/acf854b636e0b8e5a12c3f9951d4edfa0fa73bcd/lib/lib.es5.d.ts#L1526

複雑なECMAScriptプログラミングの世界に型を持ち込めるようになったのは、TypeScriptが1.0のリリースより前からジェネリクスを採用したおかげです。ジェネリクスはTypeScriptの真髄です。もし周りにTypeScriptのジェネリクスに対して抵抗感を抱いている仲間がいれば、ぜひこのアドベントカレンダーを紹介していただければ筆者として幸いです。

明日は『実例 extends Error

明日もノンジャンルを取り扱います。TypeScriptのtry-catch文にはどういった特徴があるのか、そしてその特徴があるのはなぜなのかについて紹介します。それではまた。

Discussion