🕌

【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. コンパイル時は単なる Objectdynamic 扱いになっている

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