こんにちは!フロントエンドからバックエンドまで、TypeScriptの荒波を一緒に航海する先輩エンジニアです。
今回は、TypeScriptの型システムの中でも非常にユニークで、かつ実務でめちゃくちゃ強力な武器になる「`never`型」を使った関数制約のテクニックについてお話ししますね。
「`never`って名前は聞いたことあるけど、エラー用でしょ?」と思っていませんか?
実はこの`never`型を関数の引数に組み込むと、「特定の条件のときには、絶対にこの関数を呼び出してはいけない!」というコンパイル時の鉄壁のバリデーションが作れるんです。
ここをクリアすると、あなたの書くコードの安全性と美しさは一段上のステージに上がりますよ。さっそく、その本質を紐解いていきましょう!
—
1. 「絶対に起きてはいけない」を型にする `never` 型の本質
まず、`never`型がTypeScriptの型世界でどういう存在なのかをイメージしてみましょう。
TypeScriptの型は、その値が取りうる「集合(範囲)」を表しています。
例えば、`string`型なら「すべての文字列の集合」、`boolean`型なら`true`とジュエリーボックスに入った2つの値の集合です。
では、`never`型は何の集合でしょうか?
答えは、「空集合(値が一つも存在しない集合)」です。
[ 型の世界のイメージ ]
unknown (すべての値の宇宙)
└─ string, number, boolean (各型の世界)
└─ “hello”, 42 (具体的な値)
└─ …
└─ [ ∅ never型 ] (値が絶対に存在しないブラックホール)
値が一つも存在しないということは、「そこに値を代入することも、渡すことも物理的(型システム的)に不可能」ということです。この性質を利用して、「ここに値が来たらバグだよね」というコンパイルエラーを意図的に引き起こすのが、今回のテーマの核心になります。
—
2. 基本的な使い方:特定の引数を「出禁」にする
例えば、次のような要件を考えてみましょう。
「管理者権限(`admin`)を持つユーザー専用の処理関数がある。しかし、一般ゲスト(`guest`)のユーザーオブジェクトをこの関数に誤って渡したら、コンパイルエラーで即座に弾きたい!」
これを素朴に実装するとこうなります。
type AdminUser = { role: ‘admin’; permissions: string[] };
type GuestUser = { role: ‘guest’; restriction: string };
type AppUser = AdminUser | GuestUser;
// 管理者専用の関数
function executeAdminOperation(user: AdminUser) {
console.log(‘機密処理を実行します:’, user.permissions);
}
この状態だと、うっかり `guest` のユーザーを渡してしまっても、TypeScriptの型チェックをすり抜けて実行時エラーの原因になることがあります(文脈によってはエラーになりますが、複雑な共通型の中では見落としがちになります)。
ここに `never` 型のオーバーロードを組み合わせることで、「絶対に入力させない」という強い制約をかけることができます。
type AdminUser = { role: ‘admin’; permissions: string[] };
type GuestUser = { role: ‘guest’; restriction: string };
// 1. 通常の管理者用シグネチャ
function executeAdminOperation(user: AdminUser): void;
// 2. ゲストが渡されたら絶対にコンパイルエラーにするシグネチャ
function executeAdminOperation(user: never): void;
// 実体(実装)
function executeAdminOperation(user: AdminUser | GuestUser) {
if (user.role === ‘guest’) {
throw new Error(‘ゲストユーザーはこの操作を実行できません!’);
}
console.log(‘機密処理を実行します:’, user.permissions);
}
// — 使用例 —
const admin: AdminUser = { role: ‘admin’, permissions: [‘all’] };
const guest: GuestUser = { role: ‘guest’, restriction: ‘read-only’ };
executeAdminOperation(admin); // ✅ 正常にコンパイル・実行されます
// ❌ ここでコンパイルエラーが発生します!
// 「型 ‘GuestUser’ の引数を型 ‘never’ のパラメータに割り当てることはできません」
executeAdminOperation(guest);
すごいですね!IDE(VSCodeなど)の赤波線がバッチリ光り、`guest`を渡すことがコードを書いている瞬間にブロックされます。
—
3. なぜこれでエラーになるのか?(型評価のメカニズム)
「なんで`never`を引数に指定すると、他の型が弾かれるの?」という疑問が湧いたあなたは素晴らしい着眼点を持っています。
TypeScriptの関数オーバーロードにおいて、コンパイラは上から順に「どのシグネチャにマッチするか」を検証します。
ここで、`never`型は「どの型をも部分型(サブタイプ)として内包するが、`never`自身にはどの型も代入できない(bottom typeである)」という特殊な性質を持っています。
言い換えると、`never`型を受け入れるパラメータを持つ関数に値を渡そうとすると、TypeScriptは「その値が`never`の制約を満たせるか?」を確認します。しかし、`guest`などの具体的な型は`never`には絶対に合致しないため、「型が一致しません」というコンパイルエラー(TS2345)を吐くのです。
—
4. 実務でよくあるアンチパターンと陥りやすい文法エラー
ここで、初学者の開発者がやりがちな「惜しいミス」を見ておきましょう。
❌ やりがちなミス:実装側だけを `never` にする
// ❌ NGな例
function badFunction(param: never) {
// 実装側で never を指定しても、関数を呼び出す側の型(引数の受け口)が広ければエラーにならない
}
解説:
関数を「呼び出す側」に型制約を効かせたい場合は、単に実装の引数を`never`にするだけでは不十分です(あるいは全体が`never`になって何も渡せなくなります)。先ほど紹介したように、「特定の型を受け付けないオーバーロード」を定義するか、後述するジェネリクスとの組み合わせを使うのが定石です。
—
5. 発展編:ジェネリクスと組み合わせて「特定の型を拒絶するユーティリティ関数」を作る
もう少し実践的なテクニックとして、「特定の型以外はすべて受け付ける(あるいは特定の型だけを厳格に弾く)」ような汎用的な関数を作ってみましょう。
例えば、「数値(`number`)は絶対に受け付けないが、文字列やオブジェクトは受け付ける」というジェネリック関数を考えてみます。
// T が number の場合は never になり、それ以外は T のままになる条件付き型
type RejectNumber
function processValue
console.log(‘処理された値:’, value);
}
// — テスト —
processValue(‘Hello TypeScript’); // ✅ 文字列なのでOK
processValue({ id: 1 }); // ✅ オブジェクトなのでOK
// ❌ コンパイルエラー!
// 「型 ‘number’ の引数を型 ‘never’ のパラメータに割り当てることはできません」
processValue(42);
このパターンは、ライブラリの設計や、特定のプリミティブ型が混入してほしくないヘルパー関数を書くときに非常に役立ちます。条件付き型(Conditional Types)と`never`のコンボ技ですね。
—
おわりに
今回は関数型における `never` 型の活用法について解説しました。
- `never` 型は「値が存在しない空集合」
- 関数の引数やオーバーロードに `never` を仕込むことで、特定の型の侵入をコンパイル時に物理ブロックできる
- ジェネリクスや条件付き型と組み合わせることで、より高度な型制約デザインが可能になる
「動くコード」を書くだけなら `any` や `unknown` で逃げることもできますが、型安全の網の目をここまで緻密にデザインできるようになると、TypeScriptを書くのが何倍も楽しく、そして自信を持てるようになります。
ここをクリアできれば、あなたのTypeScriptの基礎力はもうバッチリマスターできていますよ!ぜひ明日の開発から試してみてくださいね。それでは、また次の知見でお会いしましょう!