Dart 3の真骨頂:レコード型と`typedef`で関数シグネチャを極限まで型安全にする設計手法
コードレビューをしていて、次のような関数シグネチャに出くわしたことはないでしょうか?
// 良くあるアンチパターン
Future> fetchUsers(String query, int limit, bool includeDeleted, SortOrder order) async { … }
引数が4つを超えたあたりから、呼び出し側で順番を間違えるバグ(Primitive Obsession)が頻発します。さらに、コールバック関数やイベントハンドラーが複雑なパラメータを取るようになると、コードベースは一気に「読むのが苦痛なレガシー」へと堕ちていきます。
Dart 3で導入された「レコード型 (Records)」と、古くからある「`typedef`」。この2つを組み合わせることで、クラスを乱立させることなく、極限まで型安全かつ意図が明確な関数シグネチャを設計できます。
今回は、Dart VMの内部挙動や型システムの制約を踏まえ、プロダクションコードで即座に使える洗練された設計パターンをテクニカルリードの視点から伝授します。
—
なぜクラスではなく「レコード型 + typedef」なのか?
実務の現場では、「構造体的なデータを扱うために毎回 `class` や `freezed` の `@freezed` クラスを作る」ことが行われます。しかし、単一の関数のためだけに名前付きクラスを定義するのは、ボイラープレート(冗長なコード)が増える原因になります。
一方で、素のレコード型(例: `(String, {int limit})`)をそのまま関数シグネチャに書くと、以下の問題が生じます。
1. シグネチャが長くなりすぎて可読性が落ちる
2. 同じ構造のレコードをあちこちに書くことになり、DRY原則に反する
ここで`typedef`の出番です。`typedef`に名前を与えることで、「コンパイル時の厳格な型安全性」と「圧倒的な可読性」を両立できます。
—
プロダクションコードで学ぶ設計パターン
ここでは、Webフロントエンドや非同期API連携を想定し、「検索フィルターの状態を受け取り、データを非同期でフェッチする関数」を例に取ります。
1. 堅牢な型定義と実装
import ‘dart:async’;
// ==========================================
// 1. レコード型を用いたパラメータの構造化とtypedef
// ==========================================
/// ユーザー検索のクエリパラメータを定義するレコード型
typedef UserSearchQuery = ({
String keyword,
int limit,
int offset,
bool includeInactive,
UserSortField sortField,
});
/// ソート順の定義
enum UserSortField { name, createdAt, loginCount }
/// 検索結果のメタデータを含む戻り値のレコード型
typedef UserSearchResult = ({
List
int totalCount,
bool hasMore,
});
/// コールバック用のtypedef(エラーハンドリング統合型)
typedef UserSearchCallback = Future
// ==========================================
// 2. ビジネスロジック(リポジトリ層)
// ==========================================
class UserRepository {
// typedefされたシグネチャを採用することで、引数の順序ミスや渡し忘れを完全にコンパイル時になくす
Future
// レコードの分解(Destructuring)によるクリーンなアクセス
// 名前付きフィールドなので、順番を気にする必要は一切ない
final (:keyword, :limit, :offset, :includeInactive, :sortField) = query;
// 擬似的なAPI遅延とクエリ検証
await Future.delayed(const Duration(milliseconds: 300));
if (keyword.isEmpty && limit > 100) {
throw ArgumentError(‘Limit cannot exceed 100 when keyword is empty.’);
}
// モックデータの返却
final mockUsers = [‘Alice’, ‘Bob’, ‘Charlie’]
.where((u) => u.toLowerCase().contains(keyword.toLowerCase()))
.toList();
return (
users: mockUsers,
totalCount: mockUsers.length,
hasMore: false,
);
}
}
// ==========================================
// 3. UI / クライアント層での利用
// ==========================================
void main() async {
final repository = UserRepository();
// 呼び出し側:名前付き引数形式でレコードを構築するため、自己文書化(Self-documenting)される
final query: UserSearchQuery = (
keyword: ‘Dart’,
limit: 10,
offset: 0,
includeInactive: false,
sortField: UserSortField.createdAt,
);
try {
// 実行
final result = await repository.search(query);
// パターンマッチング風の分解代入で結果を受け取る
final (:users, :totalCount, :hasMore) = result;
print(‘Fetched ${users.length} users (Total: $totalCount). Has more: $hasMore’);
} catch (e) {
print(‘Error executing search: $e’);
}
}
—
コードレビューの視点:なぜこの設計が優れているのか?
A. コンパイル時安全性と構造的型付け (Structural Typing)
Dartのレコード型は構造的型付けです。つまり、同じフィールド名と型を持つレコードであれば、異なる `typedef` 名であっても代入互換性を持ちます(※ただし、Dartの `typedef` はエイリアスなので、可読性と言語サーバーの補完を強制するためにエイリアスを通します)。これにより、予期せぬ型キャストのエラー(`TypeError`)が実行時発生する余地を完全にコンパイル時になくします。
B. メモリ効率とパフォーマンスの最適化
「じゃあ、こういうデータ構造は全部 `class` や `freezed` でいいじゃないか」と思われるかもしれません。しかし、`class` はヒープ上にインスタンスが生成され、GC(ガベージコレクション)のプレッシャーになります。
一方、Dartのレコードは軽量な値オブジェクト(Value Object)です。小規模なレコードはインラインで最適化されやすく、不要なオブジェクト生成コストを抑制できます。特にUIの再描画頻度が高いFlutterや、高スループットが求められるサーバーサイドDartにおいて、GCストールを軽減する強力な武器となります。
C. デストラクチャリング(分解)のシナジー
関数内でパラメータを受け取った際、`query.keyword` のようにドットアクセスする必要はありません。
final (:keyword, :limit) = query;
このように一瞬でローカル変数に展開できるため、コードのボイラープレートが劇的に削ぎ落とされます。
—
実務で気をつけるべきアンチパターンと注意点
最後に、シニアエンジニアとしてこのパターンを導入する際の注意点を共有します。
1. レコードの肥大化に注意する
フィールド数が6個、7個と増えていくレコードは、もはや「単なるパラメータの束」ではなく、ドメインモデル(ビジネスロジックを持つべき実体)であるべきです。その場合は潔く `class` や `@freezed` を選択してください。「目安は最大4〜5フィールドまで」です。
2. ミュータビリティの誤解
Dartのレコードはイミュータブル(不変)です。フィールドの値を後から書き換えることはできません。状態を更新したい場合は、レコードの構造展開構文(Record update syntax)を使います。
final updatedQuery = (
…query,
keyword: ‘Flutter’, // keywordだけ上書き
);
—
まとめ
Dart 3のレコード型と`typedef`の組み合わせは、関数シグネチャの「意味不明な引数の羅列」という悪しき習慣を根絶するための特効薬です。
- 引数の順序ミスをコンパイルエラーで防ぐ
- 無駄なクラス定義を減らし、コードベースを軽量に保つ
- デストラクチャリングによって圧倒的な可読性を手に入れる
明日のコードレビューから、多すぎる引数を持つ関数を見つけたら、このパターンへのリファクタリングを提案してみてください。チーム全体のコード品質が一段階上のステージへと引き上げられるはずです。