Mapping Modifiersと実例 Writable<T, K> / TypeScript一人カレンダー
こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の11日目です。昨日は『readonly
とReadonlyArray<T>
』を紹介しました。
テストでちょっと困る型の扱い2
昨日紹介したreadonly
を常に指定していく筆者のスタイルは、多くの場合で問題にならないのですが唯一問題となった箇所が存在します。それは単体テストのときです。テストでちょっと困るという話題は以前runRenderHook()
を紹介したときにもしましたので、その第2弾です。
筆者の関わった案件では、JSON SchemaとAjvを組み合わせてバックエンドのレスポンス処理を検証していました。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として定められています。
& 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