【入門編】Dartの「Never」型をパターンマッチングのデフォルトケースで活用する – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの開発を楽しんでいますか?
今回は、Dart 3でさらに強力になった「パターンマッチング」と、Dartの型システムの一番底にある「`Never`型」を組み合わせた、ちょっとカッコよくて実務でめちゃくちゃ役立つ高度なテクニックのお話です。

「他の言語からDartに来たけれど、網羅的なパターンマッチングでどう書くのが正解なの?」
「エラーハンドリングをもっとスマートに、コンパイルエラーで守ってもらいたい!」

そんな疑問を持っているあなたへ。ここをクリアすれば、Dartの型システムを完全に手なづけたと言っても過言ではありませんよ。さっそく、その本質を紐解いていきましょう!

—

1. そもそも `Never` 型ってなに?(おさらい)

Dartの型階層のピラミッドを思い浮かべてみてください。一番上にすべての型の親である `Object?` があり、一番下(底辺)にそびえ立つのが `Never` 型 です。

`Never` とは文字通り「絶対にそこに到達しない(値が存在しない)」ことを表すボトム型です。
例えば、常に例外を投げる関数や、無限ループに入る関数は、正常な値を決して返さないため、戻り値の型が `Never` になります。

// この関数は絶対に値を返さず、例外をスローして処理が中断します
Never throwError(String message) {
throw Exception(message);
}

この「絶対に値を持たない・到達しない」という性質が、Dart 3のパターンマッチングと出会うことで、驚異的なパワーを発揮するんです。

—

2. Dart 3 パターンマッチングの「網羅性(Exhaustiveness)」

Dart 3で導入された `switch` 式やパターンマッチングの素晴らしい特徴の一つに、「網羅性チェック(Exhaustiveness checking)」があります。

例えば、以下のような「状態」を表すシーフ(密封)クラスやEnumがあるとします。

enum NetworkState { loading, success, error }

String getMessage(NetworkState state) {
return switch (state) {
NetworkState.loading => ‘読み込み中…’,
NetworkState.success => ‘成功しました!’,
// ここで error を書き忘れると、コンパイラが怒ってくれます!
};
}

Dartのコンパイラは非常に優秀なので、「おいおい、`NetworkState.error` のケースが抜けてるよ!」とコンパイルエラーを出してくれます。これがバグを未然に防いでくれる最大の理由ですね。

—

3. 「絶対にありえない未来」を `Never` でコンパイラに教える

では、ここで少し踏み込んだシチュエーションを考えてみましょう。

APIのレスポンスなどで、型としては `String` を受け取るけれど、ビジネスロジック上「特定の特殊な値は絶対にここには入らない(はず)」というケースや、将来的にEnumに値が追加されるかもしれないけれど、現在のコードでは想定外のケースを扱いたいときです。

ここで登場するのが、「デフォルトケース(`_`)に `Never` を返す関数を配置する」というテクニックです。

実際のコードを見てみましょう。

enum Role { admin, user, guest }

String getDashboardMessage(Role role) {
return switch (role) {
Role.admin => ‘管理者ダッシュボードへようこそ’,
Role.user => ‘ユーザー画面です’,
Role.guest => ‘ゲスト様、ログインしてください’,
// 万が一、想定外の値が入ってきた場合の処理
_ => handleUnexpectedRole(role),
};
}

// 決して到達しないはずのコードパス
Never handleUnexpectedRole(Object? unknown) {
// ここに到達するということは、想定外のデータ構造やバグが存在する
throw StateError(‘致命的なエラー: 未知のロールが検出されました -> $unknown’);
}

このコードの何がスゴいのか?

1. 意図の明確化: デフォルトケース(`_`)に「ここには絶対に来ないはずだ」という意図を `Never` 型の関数(あるいは直接スロー)で明示しています。
2. 静的解析の完全な味方: もし将来、`Role` に `superAdmin` が追加されたとします。もし `switch` 式にそれを書き忘れた場合、Dartのコンパイラは「あれ、デフォルトケースの `_` があるから網羅はされているけど…もし新しい値が来たら `handleUnexpectedRole` に流れるな」と判断します。
3. 安全なフェイルファスト: 実行時に想定外のデータが流れてきた場合、黙って無視される(サイレントバグ)のではなく、即座に `StateError` を投げてアプリをクラッシュ(またはキャッチ可能な例外をスロー)させ、開発初期段階でバグに気づくことができます。

—

4. 陥りやすい罠とスマートな書き方

初心者のうちは、「とりあえずデフォルトケースに `throw` を直書きしておけばいいや」と思いがちです。

// ❌ ちょっともったいない書き方
String badExample(Role role) {
return switch (role) {
Role.admin => ‘管理者’,
Role.user => ‘ユーザー’,
Role.guest => ‘ゲスト’,
_ => throw Exception(‘ありえない値’), // 型推論が曖昧になりがち
};
}

もちろんこれでも動きますが、`throw` 式はDartでは `Never` 型に評価されるため動くものの、複雑なswitch式や、将来的にロジックを拡張・リファクタリングする際には、専用のハンドリング関数に切り出すか、明示的に `Never` を戻り値に持つヘルパー関数を用意する方が、コードの意図がドキュメントとしても美しく残ります。

また、`Never` 型を使うときは、「本当にそこが到達不能(Unreachable)であるか」を自分自身で確信している必要があります。もし「もしかしたら通るかも」という場所で `Never` を使うと、意図しないアプリのクラッシュを引き起こすので注意してくださいね。

—

まとめ:Dartの型システムを使いこなそう

今回は、Dartの `Never` 型をパターンマッチングのデフォルトケースで活用する極意について解説しました。

  • `Never` 型 は「絶対に値が存在しない・到達しない」ことを表すボトム型。
  • Dart 3の `switch` 式の網羅性チェックと組み合わせることで、堅牢なエラーハンドリングを実現できる。
  • 到達不能なパスを明示することで、将来の仕様変更やバグの混入をコンパイル時に検知しやすくなる。

ここをクリアできれば、あなたの書くDartコードは、単に「動くコード」から「コンパイラに守られた堅牢なコード」へとワンランクアップします。

ぜひ、実際のプロジェクトの `switch` 式で試してみてくださいね。応援しています!

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