【Flutter】型推論が効かない!推論の分水嶺は外部との境界
はじめに
Flutterで開発をしていると、Web API(JSON)やFirebase、ローカルDBなど「外部との境界」をまたぐときに、急に型推論が効かなくなることはありませんか?
特に、中身は配列のはずなのに「List<dynamic>とすら判断されない」「リストのメソッドが使えない」という現象は、多くの人が一度は直面したトラブルかと思います。
この記事では、なぜこの現象が起きるのか、そしてどうすれば安全に解決できるのかを整理しました。
なぜ型推論が効かなくなるの?
そもそもの原因は、Dartのコンパイラ(コードを解析する機能)が、コードを書いている時点では「外部からどんなデータが返ってくるか」を知ることができない点にあります。
jsonDecode の戻り値は dynamic
基本的に、jsonDecodeなどのデコード関数から返ってくる値は dynamic 型です。
dynamic とは、「型チェックを一時的に無効にする」型のこと。コンパイラは「実行してみるまで何かわからないから、とりあえず何でも許可しよう(でも推論はしないよ)」という態度をとります。
そのため、APIから [1, 2, 3] という配列が返ってきていても、変数が dynamic として扱われていると、IDEの補完(.mapや.lengthなど)が出なかったり、実行時に「型が違う」と怒られたりしてしまいます。
「List<dynamic>とも判断されない」原因
「リストだと思っているのに、リストとして扱えない」というケースには、主に3つのパターンがあります。
A. コンパイル時は単なる Object や dynamic 扱いになっている
JSONデコード直後のデータは、中身がリストであっても、変数の型情報としては「何でもあり(dynamic)」の状態です。Dartはこれを「確実にListだ」とは断定できないため、List特有のメソッド(add や [])を使おうとすると警告が出たり、補完が効かなかったりします。
B. List<dynamic> は List<String> ではない (型の不一致)
ここが一番ハマりやすいポイントかもしれません。
// APIからの戻り値(実行時の実体は List<dynamic>)
dynamic response = jsonDecode('["a", "b", "c"]');
// エラー! List<dynamic> を List<String> に直接代入はできません
List<String> myData = response;
Dartでは、List<dynamic>(なんでも入るリスト)を List<String>(文字しか入らないリスト)として扱うのは危険なため、自動的な変換を行いません。これにより「リストなのに変数に入らない(リストとして認識されていない気がする)」という現象が起きます。
C. Map として解釈されている
JSONが [ ... ] ではなく { "data": [...] } のような形式の場合、トップレベルは List ではなく Map<String, dynamic> になります。単純な勘違いですが、dynamic 型で受けているとキーを指定するまで気づきにくいことがあります。
対処法
外部データを扱うときは、「推論に頼らず、明示的に型を指定・変換する」のが一番の近道です。
Dioを使った実践的な例
Web開発でよく使われるライブラリ「Dio」を使う場合も、考え方は同じです。
以下のコードを見てみましょう。response.data(dynamic型)を受け取った直後に、「これはリストですよ」と明示的にキャストしているのがポイントです。
Future<List<User>> fetchUser() async {
// 1. GETリクエスト
final response = await _dio.get('/users');
// 2. ここが重要!
// dynamic型のままだと .map が使えないことがあるため、
// 「これは List<dynamic> です」と明示的にキャストします。
final usersJson = response.data as List<dynamic>;
// もしくは型を明示して代入
final List<dynamic> usersJson = response.data;
// 3. リストとして認識されたので、map で User オブジェクトに変換できます
return usersJson.map((json) => User.fromJson(json as Map<String, dynamic>)).toList();
}
こうすることで、IDE(エディタ)も「ああ、これはリストなんだな」と理解してくれるため、コーディング中にエラーが出たり補完が効かなくなったりするストレスから解放されます。
おわりに
Flutter開発において、外部データとの連携は避けて通れませんが、そこで発生する「型推論の断絶」は多くの開発者を悩ませるポイントです。
今回解説した通り、APIなどから返ってくるdynamic型は、一時的な「何でもあり」の状態にすぎません。コンパイラが中身を特定できない以上、私たち開発者がコードの中で「これはリストです」「これはUser型です」と明示的に教えてあげる必要があります。
特に重要なのは、「データを受け取った直後(境界)」で型を確定させることです。
ここでdynamicのまま処理を流してしまうと、アプリの奥深くで予期せぬ実行時エラーが発生したり、IDEの便利な補完機能が使えなかったりと、開発効率を大きく落とす原因になります。
逆に言えば、入り口でしっかりとList<dynamic>へのキャストや、fromJsonによる型変換を済ませてしまえば、Dart本来の強力な型安全性という恩恵をフルに受けることができます。
Discussion