🎄

実例 ExtractKeyOf / TypeScript一人カレンダー

2024/12/18に公開

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

Extractの弱点とキー操作

昨日はOmit<T, K>型について、その弱点とStrictOmit型による克服を紹介しましたが、同様の問題はExtract<T, U>にも潜んでいます。Extract<T, U>は、共通する部分(交差部分)を取り出すためのユーティリティですが、Omit型がキーに特化しているのに対し、Extract<T, U>型のU(第二型パラメータ)は任意の型に対して機能します。つまりUにオブジェクトが渡ることもあり得ます。これ自体は合理的ですが、keyofとの組み合わせでExtract<keyof Obj, SomeKey>のような書き方をすると、昨日紹介したOmit<T, K>と似た懸念が生じます。

存在しないキーをExtract<T, U>Uに指定してもコンパイラがエラーを出してくれず、意図しない指定の除去が忘れられ、いつまでも残存し続ける状況が起こり得ます。このようなユースケースではExtract単体では柔軟すぎるため、厳密なキー操作が求められる場合には対応が不十分なのです。

ExtractKeyOf<T, K> を自作

Omitに対してはStrictOmitという便利な型がts-essentialsに用意されていましたが、同様にExtract<keyof Obj, SomeKey>を厳密にチェックする型については、筆者が知る限り前例がありませんでした。そこで、筆者はExtractKeyOf<T, K>なる型を自作しました。

このExtractKeyOf<T, K>は、KTのキーとして実在するかどうかを厳密に検証し、存在しない場合はエラーを誘発するような設計になっています。以下はその一例(擬似的なコード例)です。

type ExtractKeyOf<T, U extends keyof T> = Extract<keyof T, U>;

type Obj = {
  a: string;
  b: number;
  c: boolean;
};

type ValidKey1 = Extract<keyof Obj, "b">; // "b"
type InvalidKey1 = Extract<keyof Obj, "d">; // エラーになってほしいが、ならない

type ValidKey2 = ExtractKeyOf<Obj, "b">; // "b"
type InvalidKey2 = ExtractKeyOf<Obj, "d">; // エラー: "d"はObjのキーではない

このような型を自作することで、Extractで行いたかった厳密なキー制約を表現できます。Extract<keyof Obj, SomeKey>のようなkeyofを同伴する場面では、積極的に用いたいものです。

Rustから学んだ「エラーは学びの源」

筆者はRustを学んだ際、Cargo標準のコンパイルエラー出力が極めて丁寧である点に感銘を受けました。Rustは習得が難しいといわれることもありますが、エラーメッセージが充実しているためエラーを読む過程で学習が進みます。この経験から、コンパイルエラーが「学びのガイド」になり得ると再認識しました。

TypeScriptでも同様に、開発中にエラーを見かけることは決して恥や面倒なことではなく、むしろ開発者を正しい道へ導く幸せな瞬間といえます。コンパイルエラーが発生するたびに「なぜエラーが出たか」「どう直せば型が整合するか」を考えることで、コードベースが徐々に洗練され、ミスが減っていくのです。

自作型作成の指針

今回はts-essentialsには存在しないユースケースを実現するために、自作型を定義しました。ではどういったときに標準ユーティリティや既存のOSSユーティリティ集に頼り、どういったときに自作すればよいのでしょうか。

自作型を定義する際には、「TypeScriptコンパイラにどれだけ多くのミスを検出してもらうか」を意識すると良いバランスがとれます。闇雲に複雑な型を量産してしまうと、むしろ型スパゲッティになり、誰も手を触れたがらないコードになりかねません。

重要なのは「自作型を作らなければ、どんなケアレスミスが起こるだろうか」を考えることです。言うなれば「どうすればコンパイルエラーを通じて周りの開発者を支援するか」を考えるのです。そして、それらのミスを型レベルで捕捉する仕組みを作り、エラーを積極的に出すことによって開発者に誤りを気付かせます。

これは最初期のTypeScriptで、すべてがanyであるJavaScriptプログラミングにstringnumberといった基本型のアノテーションを大量につけまわしていた世代とは考え方が全く異なっており、より高度な型を組み合わせて問題を未然に防ぐために積極的に自動化していく世代へと移行している、ということです。

これは2年前のカレンダーで紹介したテクニックをより発展させることにも役立ちます。たとえば、単純にプロパティとその値の型の一致を検証するだけでなく、その型が何によってそうなっているのかという依存の因果関係を明記する。これにはParametersReturnTypeが有用です。

TypeScriptの型ユーティリティに積極的に慣れておくと、古くTypeScriptとして一般的だった考え方としての「型が合っているかどうか」だけでなく、より論理的に「なぜ誤りだと判明できて、その根拠はどの宣言に基づくのか」という追跡性の向上に繋がります。もうどのファイルのどの辺が怪しいか勘で当てていく必要はありません。すべて、TypeScriptのコンパイルエラーに表示された行番号を見るだけでよくなります。

自作型を設計するときは「自分を含め全ての開発者はどんな誤りをするだろうか」と常に先回りして、開発者が犯しがちなエラーはTypeScriptの力で自動で検出できるようにする。そうすることで、レビュー時や保守時の負担が軽減され、チーム全体が幸せになるコードベースを築けます。TypeScriptの上達は、こういった支援がうまく機能しているときに実感します。

明日は『実例 is()』

本日は「実例 ExtractKeyOf」を紹介しました。明日は「実例 is()」を紹介します。それではまた。

Discussion