🎄

StrictOmit / TypeScript一人カレンダー

2024/12/18に公開1

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

Omit<T, K>の弱点

2年前のカレンダーでは、Omit<T, K>型を紹介したことがあります。Omit<T, K>は、Tから指定したキーKを除外した新たな型を返す便利なユーティリティです。ところが、Omit<T, K>には弱点があります。

Pick<T, K>が指定したキーKが存在しない場合にエラーを出してくれるのに対し、Omit<T, K>はそうした厳密なチェックを行いません。言い換えると、存在しないキーをOmitに指定してもコンパイラは黙って通してしまいます。

次のコード例をご覧ください。

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

type Picked = Pick<Obj, "d">; // エラー: "d" は Obj型 に存在しない
type Omitted = Omit<Obj, "d">; // エラーとならない

Picked型の宣言ではObjの第二型パラメータに存在しないキー"d"を指定しているためコンパイラはエラーを出します。ところが、Omitted型では第二型パラメータに同じように存在しないキー"d"を指定してもエラーになりません。その結果、無効な指定をしたままにしても気付けず、後々まで気付かないまま放置されてしまうことがあります。

StrictOmit型で厳密なエラーチェック

2年前にOmit<T, K>を紹介したあと、筆者は何度か「消し忘れ」や「存在しないキーが指定され続けている」といった問題に直面しました。

こうしたエラーをコンパイル時点で検出してくれないと、実際の型定義と整合しない指定が残存してしまい、保守性や可読性を損ねてしまいます。古くからいる開発者にとっては単なる消し忘れに見えても、新しく入ってきた開発者にはなぜ存在しないキーが平然と書かれているのか判断できないためです。コードを読んでいる上で、脳の負担となる状況は多くの場合目の前のコードに対して「なぜ?」が浮かぶ状況です。こういった負担を取り除くため、不要なキーの指定は、不要になったタイミングですべて除去したいものです。

そこで、昨日の記事で紹介したts-essentialsというユーティリティ集が本日も役立ちます。ts-essentialsにはStrictOmit<T, K>という型が用意されており、Omit<T, K>よりも厳密にキーをチェックしてくれます。これを使うことで、存在しないキーを指定した場合にコンパイルエラーとなり、前述のような問題を未然に防ぐことができます。

import type { StrictOmit } from 'ts-essentials';

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

type Picked = Pick<Obj, "d">; // エラー: "d" は Obj型 に存在しない
type Omitted = Omit<Obj, "d">; // エラーとならない
type StrictOmitted = StrictOmit<Obj, "d">; // エラー: "d" は Obj型 に存在しない

このように振る舞う理由は簡単です。ts-essentialsの実装を実際に確認してみましょう。

export declare type StrictOmit<
  Type extends AnyRecord,
  Keys extends keyof Type,
> = Type extends AnyArray ? never : Omit<Type, Keys>;

ここで第二型パラメータKeyskeyof Typeの制約を備えていることに注目します。keyofは2年前のカレンダーにて紹介しています。TypeScript標準のOmit<T, K>では、ここがanyとなっている点が異なります。

anyと違い、このkeyof制約が足されていることによって、Pick<T, K>と同様にキーが存在するかどうか検証されるようになるのです。

TypeScript標準ユーティリティの設計意図と実務での選択

Omit<T, K>に限らず、TypeScriptの標準の型定義やユーティリティは比較的広めの設計になっています。たとえば代表的なものを挙げると、JSON.parse()の戻り型はTypeScriptの最初期からany型とされています。 これらは歴史的経緯であったり、汎用性を高く保つためであり、その結果すべての場面で厳密なチェックが行われるとは限りません。そのため標準の型をそのまま使った場合、特定の実務上のシナリオで望まぬ挙動、すなわちエラーが出てほしい時に出てくれないといったことが発生します。

TypeScript標準の型定義が全ての状況で常に正解であると筆者は考えていません。実務では、「もしこうなったときにはエラーにしたい」という場面があるのなら、その挙動を実現するためのユーティリティ(ts-essentialStrictOmit`のようなもの)を導入したり自作したりして活用することが有用です。

標準ユーティリティ型には便利なものが豊富ではありますが、これだけでまかなおうとせず、自分たちの要件やチームの方針に合わせて更なる工夫を凝らし、型エラーを積極的に味方につけることで、より安全で保守しやすい型定義が可能になるのです。

明日は『実例 ExtractKeyOf』

本日は「StrictOmit」を紹介しました。明日は『実例 ExtractKeyOf』を紹介します。それではまた。

Discussion

michiharumichiharu

「こうしたエラーをコンパイル時点で検出してくれないと..」の件は非常に共感します。

昨日、偶然にも StrictOmit を考えついてこれは便利だと喜んでいたら、昨日の記事で StrictOmit が予告されていたので既に世の中にあったことを知りました😇そして記事が投稿されるのを楽しみにしていました。

ts-essential いいですね!使ってみようと思います。