【実務・中級編】 neverを用いた型のフィルタリング – TypeScript実践ガイド

やあ、元気にしてるかな。現場でコードをバリバリ書いていると、必ずぶち当たる壁がある。それが「型が広すぎて扱いづらい」という問題だ。

APIから返ってくるユニオン型、あるいは複雑に組み合わさった既存の型定義……。それらの中から「特定の不純物」だけを綺麗に取り除き、純度の高い型を取り出したい。そんな時に、僕らシニアエンジニアが密かに、そして当たり前のように使いこなしているのが「Conditional Typesと`never`を組み合わせたフィルタリング」だ。

今日は、公式ドキュメントをなぞるだけでは決して辿り着けない、このテクニックの本質と「なぜそう動くのか」という裏側のメカニズムを徹底的に解説しよう。

—

なぜ `never` なのか? その真の正体を知る

まず、君に問いかけたい。`never` 型を単なる「値が存在しないことを示すエラー用の型」だと思っていないかい?

確かに、関数の戻り値が例外を投げる場合などに `never` は使われる。しかし、型定義の文脈において `never` は「集合論における空集合(Empty Set)」として機能するんだ。

ここが重要なポイントなのだが、TypeScriptのユニオン型において、`never` は「合体しても相手を変化させない(恒等元)」という性質を持っている。

type Result = string | number | never;
// これの結果は ‘string | number’ になる。neverは消滅するんだ。

この「特定の条件で `never` を返せば、その型はユニオンから消える」という性質を利用するのが、型フィルタリングの極意だ。

—

魔法の杖:Distributive Conditional Types(分配法則)

さて、次に理解すべきは「条件付き型(Conditional Types)」の挙動だ。

type IsString = T extends string ? true : false;

これ自体は単純だが、ジェネリック型 `T` にユニオン型を渡したとき、TypeScriptは魔法のような動きを見せる。これを「分配(Distributive)」と呼ぶ。

もし `T` が `string | number` だった場合、TypeScriptは内部でこう解釈するんだ。
`(string extends string ? true : false) | (number extends string ? true : false)`

結果として `true | false` になる。この「一つずつバラして判定する」という性質と、先ほどの「`never` は消える」という性質を組み合わせると、特定の型を間引くフィルターが完成する。

—

実践:特定の型を型定義から「蒸発」させる

では、現場ですぐに使える実用的なコードを見ていこう。例えば、複雑なレスポンス型から「関数」や「null」だけを綺麗に取り除きたいケースを想定してみる。

/

  • 渡されたユニオン型 T から、型 U に割り当て可能な要素を「除外」する
  • これが組み込みの Exclude の正体だ。

/
type MyExclude = T extends U ? never : T;

/

  • 逆に、特定の型「だけ」を抽出するフィルター

/
type MyExtract = T extends U ? T : never;

// — 現場での活用例 —

type RawData = string | number | (() => void) | null | undefined;

// 1. 関数とnull/undefinedを排除して、純粋なデータ型(string | number)だけを取り出す
type CleanData = MyExclude;

// 2. string型だけに絞り込む
type OnlyString = MyExtract;

/

  • 【解説】
  • MyExclude が動くとき、内部ではこうなっている:
  • 1. (string extends Function ? never : string) => string
  • 2. (number extends Function ? never : number) => number
  • 3. (() => void extends Function ? never : (() => void)) => never
  • 4. …
  • 最終的に string | number | never | … となり、neverは消えるため
  • 結果として string | number だけが残る。美しいだろう?

/

—

ブラウザの裏側とTypeScriptの「消去」

ここで少し、アーキテクトらしい視点を共有しておこう。

こうした `never` を使った高度な型パズルは、ブラウザ上で実行されるときには影も形も残っていない。 TypeScriptの型システムは、コンパイル(トランスパイル)時にすべて「消去(Erasure)」されるからだ。

「じゃあ、実行時のパフォーマンスには関係ないのか?」

その通り。しかし、「開発時の安全性」と「コードの自己文書化」という点では、計り知れない恩恵がある。
不適切な型が混じり込むのをコンパイル段階で阻止できれば、実行時に `undefined` のプロパティを叩いてブラウザがクラッシュする(あの忌々しい `Uncaught TypeError` だ)可能性を、デプロイ前にゼロに近づけることができるんだ。

—

応用編:オブジェクトのプロパティをフィルタリングする

中級者の君なら、もう一歩踏み込んでみよう。オブジェクトのキーを型でフィルタリングする手法だ。これは実際のライブラリ開発や、大規模な状態管理(ReduxやZustandなど)の設計で頻出する。

/

  • オブジェクト T から、値の型が V であるプロパティ名だけを抽出する

/
type KeysOfType = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

// 実践的なサンプル
interface UserProfile {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
updateEmail: (newEmail: string) => void;
}

// UserProfileの中から「文字列型」のプロパティ名だけを抜き出す
// 1. まず { id: never; name: “name”; age: never; email: “email”; … } という型ができる
// 2. その後、値のユニオンを取ることで never が消え、”name” | “email” だけが残る
type StringKeys = KeysOfType; // “name” | “email”

// これを使えば、特定の型だけに反応するコンポーネントのPropsなどが作れる
function UpdateField(key: KeysOfType, value: string) {
// id や updateEmail を渡そうとするとコンパイルエラーになる。安全だ。
console.log(`Updating ${key} to ${value}`);
}

UpdateField(“name”, “田中”); // OK
// UpdateField(“age”, “30”); // Error: “age” は string ではないので弾かれる

—

シニアからのアドバイス:やりすぎには注意

`never` を使ったフィルタリングは強力だ。しかし、あまりに複雑な型パズルを組み上げすぎると、後輩たちがコードを読めなくなる「型パズル地獄」に陥ることもある。

コツは、「意味のある単位で型に名前をつけること」だ。
`T extends U ? never : T` と直接書くのではなく、`ExcludeData` のように意図が伝わるエイリアスを定義してあげてほしい。

型定義は、未来の自分やチームメンバーへの「手紙」だ。
論理的に厳密でありつつ、人間が読んでも意図が伝わる。そんな「優しくも強い」コードを書けるようになることが、真のフロントエンド・スペシャリストへの道だと僕は思う。

今回の `never` の使いこなし、ぜひ君のプロジェクトの型定義ファイル(`types.d.ts`)に忍ばせてみてくれ。きっと、コードの堅牢さが一段階上がるはずだ。

次は `infer` キーワードを使った型推論の深淵について話そうか。また会おう!

コメント

タイトルとURLをコピーしました