🎄

Mapping Modifiersと実例 Writable<T, K> / TypeScript一人カレンダー

2022/12/21に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の11日目です。昨日は『readonlyReadonlyArray<T>』を紹介しました。

テストでちょっと困る型の扱い2

昨日紹介したreadonlyを常に指定していく筆者のスタイルは、多くの場合で問題にならないのですが唯一問題となった箇所が存在します。それは単体テストのときです。テストでちょっと困るという話題は以前runRenderHook()を紹介したときにもしましたので、その第2弾です。

筆者の関わった案件では、JSON SchemaAjvを組み合わせてバックエンドのレスポンス処理を検証していました。JSONの世界はTypeScriptとは異なりますので、あるべきプロパティを書かない、想定と違う型の値を代入するなど、なんでもありになってしまいます。そこでJSON Schemaを採用して定義を作成し、その定義に基づいてバリデーション処理を実施するAjvを組み合わせることで、なんでもありにならないように制限することができます。

そういった状況では、意図的にプロパティを落としてJSON Schemaがそれをエラーとする検証を、テスト上でTypeScriptとして表現しにくくなります。any型を使えば可能になるのはもちろんとして、TypeScriptを使う上でany型はなんとしても避けたいです。そこで思いつくのはOmit<T, K>ですが、Omit<T, K>だと最初からプロパティとして存在していない定義は書けたとしても、delete演算子での操作が書きにくくなります。TypeScriptではdelete演算子は定義されたプロパティにしか使用できないからです。

では仕方なくreadonlyをつけないようにするという判断もできますが、プロダクトコードではwriteしないにも関わらずテストコードのためにreadonlyを外すというのも悩ましい。そこで自作Utility Typeを定義しました。

Writable<T, K>

Writable<T, K>型を自作することで、テスト中にちょっとだけread-writeを許容したいという状況でも扱えるようにしました。実装を確認しましょう。

export type Writable<T, K extends keyof T> = {
  -readonly [P in K]: T[P];
} & Omit<T, K>;

複雑そうな見た目になっていますが、ひとつずつみるとここまでのアドベントカレンダーの記事群で紹介したものばかりの構成です。[P in K]: T[P]の書き方はMapped Typesです。そこで紹介したPick<T, K>の実装を思い出してください。

-readonlyは今回初登場の記法で、これはMapped Typesにおいてreadonlyが付いているものに該当するのであればそれを外す、という指示子です。この仕様はMapping Modifiersとして定められています。

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers

& Omit<T, K>の部分は、まさにOmit<T, K>そのものです。つまりこのWritable<T, K>Pick<T, K>によく似ており、かつOmit<T, K>と合成されているということがわかります。その結果、挙動がどうなるかをみてみましょう。

type Obj = {
  a: string;
  readonly b: string;
  readonly c: string;
};

type T11 = Writable<Obj, "a">;
//   ^? Obj

type T12 = Writable<Obj, "b">;
//   ^? { a: string; b: string; readonly c: string; }

type T13 = Writable<Obj, "b" | "c">;
//   ^? { a: string; b: string; c: string; }

このように、Kに指定したプロパティのreadonlyのみを外し、それ以外のプロパティはそのままという扱いになります。こうすることで、プロダクトコード上ではreadonlyを付けておき、テストコード上ではany型にすることなく一時的にreadonlyを外すということが可能になりました。

AjvやJSON Schemaを扱ったTypeScript境界外の挙動の検証処理を、どうTypeScriptのテストコードとして記述すべきかという議論は、別途盛り上がりそうな議論ではあります。突き詰めていくとJSON Schema自体に誤りがないことを示すテストについても配慮せねばなりません。この辺りは本稿の主旨とは外れてくるため、またの機会とします。

次回は『Partial<T>

本日はMapping Modifiersという仕様と、これまでに紹介してきたUtility Typesを組み合わせることで、ちょっとした不便を満たすための自作Utility Typesを定義できることを紹介しました。明日はまだまだあるMapped TypesのひとつPartial<T>を紹介します。それではまた。

Discussion