「型設計ルール」を決めておく意味
最初に、なぜ「ルール」を意識した方がいいかから話します。
TypeScript は、
とりあえず書いてもそれなりに動いてくれる言語です。
でも「何となく型を付ける」だけだと、すぐにコードがごちゃごちゃになり、
- どこで null が来るか分からない
- union が増えすぎて毎回 if 地獄
- どの関数が何を期待しているか読みにくい
という状態になりがちです。
逆に、基礎の段階で「こういうときはこう型を付ける」と自分なりのルールをもっておくと、
その後の学習も実務もかなり楽になります。
ここでは、まさに基礎レベルで意識しておくと効く「型設計のルール」を、
具体例と一緒にかみ砕いて話します。
ルール1:まず「単純な型」で書いてから、必要になったら広げる
いきなり union にしない・いきなり optional にしない
初心者がやりがちなのが、「最初から守りに入りすぎる」ことです。
// 最初からこう書きがち
type User = {
id: string | number;
name?: string;
};
TypeScriptでも、本当に最初から string | number にする必要がありますか?
本当に名前は常に「あるかもしれないし、ないかもしれない」の設計ですか?
型設計で大事なのは、
「最初は“こうであってほしい”理想の姿をシンプルに決める」
「どうしても例外が必要になったときにだけ、型を広げる」
という順番です。
たとえば最初はこうでいいかもしれません。
type User = {
id: string; // まずは string に決める
name: string; // 名前は必須とする
};
TypeScriptそのあと、「外部APIの都合で id が number で来ることがある」と分かったら、
そこで初めて union を導入します。
type UserId = string | number;
type User = {
id: UserId;
name: string;
};
TypeScript「型を最初から“安全そうな方向に広げておく”のではなく、“理想の形を先に決めておく”」
これだけで、型の分かりやすさがかなり変わります。
ルール2:null を含めるなら「ここで処理する」と決めておく
string | null をばらまかない
TypeScript を使うと、すぐにこういう型が出てきます。
type User = {
name: string | null;
};
TypeScriptこのときにやってはいけないのが、string | null をあちこちにそのまま伝染させることです。
function greet(user: User) {
console.log(user.name.toUpperCase()); // 当然エラー
}
TypeScript基礎段階でのルールとしておすすめなのは、
「null を含む型は、“どこで null を処理しきるか”を必ず決める」 ことです。
例えば、「ユーザー情報を取得する関数」の中で null を処理しきるなら、
type User = {
name: string | null;
};
type SafeUser = {
name: string; // ここでは non-null
};
function toSafeUser(user: User): SafeUser {
return {
name: user.name ?? "名無しさん",
};
}
TypeScript以降の関数は SafeUser を前提に書けます。
function greet(user: SafeUser) {
console.log("こんにちは、" + user.name.toUpperCase());
}
TypeScriptあるいは、「この関数の中で null を弾く」と決めてもいいです。
function greet(user: User) {
if (user.name === null) {
console.log("名前がありません");
return;
}
// ここから先は string に絞られる
console.log("こんにちは、" + user.name.toUpperCase());
}
TypeScript大事なのは、
string | nullを見たら、「どこで null を処理するか」を決める- 「ここで処理する」と決めた場所で、if か
??で必ず片をつける
という習慣です。
「null を含む型は“要処理箇所”だ」と意識する、これが基礎段階の大事なルールです。
ルール3:union を作るときは「パターンを数えきれる範囲」に絞る
「なんでもアリ union」を作らない
ついこう書きたくなる瞬間があります。
type Value = string | number | boolean | null;
TypeScriptこれはほぼ確実に、あとで自分の首を絞めます。
なぜなら、Value を受け取る関数はすべて、
「string のとき」「number のとき」「boolean のとき」「null のとき」の
4パターンの責任を負うことになるからです。
基礎の段階で意識してほしいのは、
「union に含めるのは、“自分がちゃんと扱いきれるパターンだけ”にする」 ことです。
例えば、「ID は string のときもあるし number のときもある」という明確な理由があるなら、
type Id = string | number;
TypeScriptはアリです。
一方で「なんかいろんなケースがあってバラバラだから、とりあえず全部 union にしておくか」
とやると、その混乱がそのまま型にコピーされてしまいます。
余裕がないときほど、
- union の要素を 2〜3 個程度に抑える
- 「それぞれのパターンで何をしたいのか」を説明できるようにしておく
というのを目安にしてみてください。
ルール4:状態のバリエーションは Discriminated Union で書く
「状態」の設計は文字列+オブジェクトで
例えば、こういう状況を考えます。
- 読み込み前
- 読み込み中
- 読み込み成功
- 読み込み失敗
これを適当に boolean で表そうとすると、すぐぐちゃぐちゃになります。
type State = {
isLoading: boolean;
data?: string;
error?: string;
};
TypeScript基礎の段階からおすすめしたいのは、
「状態のバリエーションは、できるだけ Discriminated Union にする」 ことです。
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: string };
TypeScriptこの形にすると、
- 各状態の「持つべきもの/持ってはいけないもの」が明確になる
statusを見るだけでどの状態か分かるstatusで分岐したとき、TypeScript が型を自動で絞り込んでくれる
というメリットがあります。
「状態がいくつかある」「状態ごとに持つ値が違う」
と感じたら、できるだけこの形に寄せるのが、型設計レベルを一段押し上げてくれます。
ルール5:共通部分は「ベース型」に切り出してから足し算する
intersection型で「共通+差分」を意識する
例えば、こんな型があるとします。
type User = {
id: number;
name: string;
};
type Admin = {
id: number;
name: string;
permissions: string[];
};
TypeScriptこのままでも悪くないですが、
基礎段階から「共通部分」を意識する癖をつけると、
intersection 型 (&) を自然に扱えるようになります。
type BaseUser = {
id: number;
name: string;
};
type User = BaseUser;
type Admin = BaseUser & {
permissions: string[];
};
TypeScriptこうしておくと、頭の中のイメージがはっきりします。
- 「全ユーザーに共通する情報」=
BaseUser - 「Admin だけが追加で持つ情報」=
permissions
この「共通+差分」という視点は、
union 型と組み合わせるとさらに効いてきます。
type Person = User | Admin;
TypeScriptPerson は「共通部分は BaseUser、差分で Admin は permissions を持つ」
という構造を持った union だと理解できるようになります。
共通部分はなるべく1つの型にまとめる → 差分だけ別型で足す
というルールを持っておくと、「型が構造化されている」状態に近づきます。
ルール6:関数の引数は「その関数が本当に必要な情報だけ」に絞る
無駄に広い型を受け取らない
例えば、こんな関数があるとします。
type User = {
id: number;
name: string;
email: string;
age: number;
};
function sendBirthdayCoupon(user: User) {
console.log(user.email);
}
TypeScriptこの関数が本当に使っているのは email だけです。
にもかかわらず User 全体を要求してしまうと、
- 呼び出し側がムダに大量の情報を揃えないといけない
- テストしづらい
- 関数の役割が読み取りづらい
という問題がじわじわ効いてきます。
基礎段階で持っておくと強いルールは、
「関数は“本当に必要な情報だけ”を引数として要求する」 ことです。
例えばこう書けます。
type HasEmail = {
email: string;
};
function sendBirthdayCoupon(user: HasEmail) {
console.log(user.email);
}
TypeScriptこれなら、「この関数は email さえあれば動く」とはっきり分かります。
将来的には、Pick<User, "email"> のようなユーティリティ型も出てきますが、
基礎のうちはまず「必要な情報だけを持つ小さな型を作る」という癖をつけると良いです。
最後に:型設計ルールは「自分を楽にするための約束」
ここまでいろいろ挙げましたが、全部を一気に完璧にやる必要はありません。
ただ、どれも共通しているのは、
- 型を必要以上に広げすぎない(あとで苦しくなる)
- 「ここで処理する」と決めた場所では、null や union をちゃんとさばく
- 共通部分と差分を意識して、型を構造として捉える
- 関数は「何が必要か」「どのパターンを見るのか」をハッキリさせる
という姿勢です。
TypeScript の型は、あなたを縛る鎖ではなく、
「未来の自分の首を絞めないようにするための、今の自分からのメモ」 みたいなものです。
コードを書いていて迷ったら、
「この型の書き方は、未来の自分を楽にしているか? それとも雑に逃げているだけか?」
と一度立ち止まってみてください。
その問いを持ち続けること自体が、
型設計ルールを自分の中に育てていく、一番の近道になります。
