TypeScript | 基礎文法:Union・基本型操作 – 基礎段階での型設計ルール

TypeScript
スポンサーリンク

「型設計ルール」を決めておく意味

最初に、なぜ「ルール」を意識した方がいいかから話します。

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;
TypeScript

Person は「共通部分は 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 の型は、あなたを縛る鎖ではなく、
「未来の自分の首を絞めないようにするための、今の自分からのメモ」 みたいなものです。

コードを書いていて迷ったら、
「この型の書き方は、未来の自分を楽にしているか? それとも雑に逃げているだけか?」
と一度立ち止まってみてください。

その問いを持ち続けること自体が、
型設計ルールを自分の中に育てていく、一番の近道になります。

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