こんにちは!FlutterやDartのコードを書いていて、「ここ、どう考えてもあり得ない状態なのに、コンパイラから『網羅性チェック(exhaustiveness check)エラー』って怒られちゃったな……」と頭を抱えたことはありませんか?
他の言語からやってきた方だと、「なんでここに `default` を書かないといけないの?」とモヤモヤしがちですよね。
実はDart 3のパターンマッチングと、最強の「下限型」である `Never` 型を組み合わせると、このモヤモヤをスッキリ解消できるだけでなく、「論理的にあり得ないバグ」をコンパイル時に完全に封じ込めることができるんです。
今回は、Dartの心臓部を知り尽くした私と一緒に、`Never` 型とパターンマッチングの深淵を優しく覗いてみましょう!ここをクリアすれば、あなたのDartコードの型安全性は一段と引き締まりますよ。
—
1. そもそも `Never` 型ってなんだっけ?
Dartの型システムにおいて、`Never` 型は「絶対に値が存在しない(到達することがない)」ことを表す特別な型です。型階層のいちばん底辺(ボトム型)に位置しています。
たとえば、常に例外を投げる関数や、無限ループに入る関数の戻り値に使われますよね。
// この関数は絶対に正常終了せず、必ず例外を投げるため戻り値は Never
Never throwError(String message) {
throw Exception(message);
}
この `Never` 型のもつ「絶対にそこへ到達しない」という性質、そしてどんな型にも代入できる(サブタイプである)という性質こそが、Dart 3のパターンマッチングで最高のスパイスになるんです。
—
2. switch式と網羅性チェック(Exhaustiveness)の基本
Dart 3で導入された `switch` 式(StatementではなくExpression)は、非常に強力です。すべてのパターンが網羅されているかをコンパイラが厳格にチェックしてくれます。
例えば、次のような「ユーザーの権限(Role)」を表すenumがあるとします。
enum Role { admin, editor, viewer }
String getDashboardMessage(Role role) {
return switch (role) {
Role.admin => ‘管理者メニューへようこそ’,
Role.editor => ‘記事編集画面へようこそ’,
Role.viewer => ‘閲覧専用モードです’,
// ここで viewer を書き忘れると、コンパイルエラーになる!
};
}
「おっ、親切に教えてくれて助かるな」と思いますよね。しかし、ここに将来の拡張という罠が潜んでいます。
—
3. 未来の変更に備える:もしenumに値が追加されたら?
もし後からチームの誰かが、`Role` に `guest` を追加したとしましょう。
enum Role { admin, editor, viewer, guest }
すると、先ほどの `getDashboardMessage` 関数は、`guest` のケースを処理していないため、即座にコンパイルエラーになります。「あ、新しい権限が増えたからコードを直さなきゃいけないんだな」と気づけるので、これは非常に安全です。
しかし、次のようなシチュエーションではどうでしょうか?
「どうしても `admin` と `editor` しかこの関数では扱わない(`viewer` や `guest` が渡されることは業務ロジック上100%あり得ない、あるいは無視したい)」という場合です。
従来なら、適当に `default => ”` と書いてごまかしてしまいがちでした。これだと、将来新しいenumが追加されたときに、コンパイラが警告を出してくれず、知らないところでバグの原因(サイレントバグ)になってしまいます。
ここで登場するのが、今回の主役である `Never` 型です!
—
4. `Never` 型をデフォルトケースに置くという極意
あり得ないはずのフォールバック(デフォルト)ケースに `Never` 型の変数や関数を配置すると、コンパイラに「ここは絶対に到達しない場所だ」と強く教え込むことができます。
実際のコードを見てみましょう。
enum Role { admin, editor, viewer, guest }
String getSpecialMessage(Role role) {
return switch (role) {
Role.admin => ‘最高権限です’,
Role.editor => ‘編集権限です’,
// viewer と guest は、この関数では絶対に処理しない(想定外)
Role viewer => throwTypeError(viewer), // ← ここに注目!
Role guest => throwTypeError(guest),
};
}
// どんな値を受け取っても絶対に例外を投げるヘルパー関数
Never throwTypeError(Role role) {
throw ArgumentError(‘サポートされていないロールです: $role’);
}
……おっと、これだと結局パターンを並べないといけないので面倒ですね。
もっとスマートに、「上記以外の残りすべて(Wildcard _)」に対して `Never` 型を割り当てるイディオムがこちらです!
String getStrictMessage(Role role) {
return switch (role) {
Role.admin => ‘最高権限です’,
Role.editor => ‘編集権限です’,
// admin と editor 以外(つまり残りすべて)を Never 型として処理する
var other => throw ArgumentError(‘想定外のロールです: $other’),
};
}
ここでマジックが起きます。
もし将来、`Role` に `viewer` や `guest` が追加されたとき、Dartのコンパイラは `var other` がカバーする範囲が変わることに気づきます。しかし、もしあなたが「adminとeditor以外はあり得ない」という意図で、残りすべてを `Never` 型の処理(例外など)に結びつけておけば、型システムは厳密にそれを検証します。
より厳密に `Never` 型の型推論を利用するパターンがこちらです:
String getPerfectMessage(Role role) {
return switch (role) {
Role.admin => ‘最高権限です’,
Role.editor => ‘編集権限です’,
// 予期せぬ値が来た場合は Never を返してコンパイルを通しつつ、実行時にも絶対に落とす
_ => throw StateError(‘到達不能なコードに到達しました’),
};
}
「あれ? `_ =>` で受けてるなら、さっきの `default` と同じじゃないの?」と思われるかもしれませんが、ここがDartの型システムの奥深いところです。
`throw` 式自体が `Never` 型評価を持つため、コンパイラは「このswitch式は、すべての `Role` の値を完全に処理し尽くした(あるいは例外で弾いた)」と完璧に理解します。
—
5. 陥りがちな文法エラーと注意点
この手法を使うときに、初心者の開発者の方がよくやってしまうミスをいくつかご紹介しておきますね。
① `throw` を式(Expression)として書くのを忘れる
Dartでは、`throw` は文(Statement)ではなく式(Expression)です。そのため、アロー構文(`=>`)の後ろにそのまま書くことができます。
// ❌ 誤り:ブロック({})と文(;)を書こうとしてコンパイルエラーになることがある
_ => {
throw Exception(‘error’);
}
// ⭕️ 正解:アロー構文の右側に「式」として直結させる
_ => throw Exception(‘error’),
② 「とりあえず動くから」と `default` で思考停止しない
冒頭でもお伝えした通り、安易に `default => …` と書くのは、将来enumに新しい値が追加されたときのコンパイラの「網羅性チェック」という最強のセーフティネットを自分でブチ壊す行為に他なりません。
「ここは絶対にあり得ない」と言い切れる場所であっても、`_ => throw …` のように明確に例外を明示するか、型システムに矛盾がない状態を保つことが、プロとしての美しいコードベースを守る秘訣です。
—
まとめ:型システムを味方につけて、強靭なコードを書こう
今回は、Dart 3のパターンマッチングと `Never` 型を組み合わせて、到達不能コードをコンパイル時に保証するテクニックを解説しました。
- `Never` 型は「値が存在しない(到達不能)」を表すボトム型。
- Dart 3の `switch` 式の網羅性チェックと組み合わせることで、論理的におかしな状態を型レベルで排除できる。
- 将来のenumなどの拡張に対しても、コンパイラが変更漏れを教えてくれる強靭なコードが書ける。
「コンパイラに厳しくされるの、なんだか面倒だな」と感じることもあるかもしれませんが、それはコンパイラがあなたのコードの「一番の理解者でいてくれている」証拠です。
ここをクリアしたあなたなら、もうDart 3のパターンマッチングは怖くありません。ぜひ明日の開発から、この「`Never` 型ガード」を取り入れてみてくださいね。それではまた!