TypeScript | 基礎文法:オブジェクト基礎 – 実務でのオブジェクト設計感覚

TypeScript TypeScript
スポンサーリンク

実務でのオブジェクト設計は「現実世界のモデル化」から始まる

まず大前提として、実務でのオブジェクト設計は、「TypeScript の文法をどう書くか」より前に、「現実の世界をどう切り取るか」から始まります。
ユーザー、商品、注文、記事、コメント……扱うのはいつも「現実に存在する何か」です。

TypeScript のオブジェクト型は、その「現実の何か」の設計図です。
だから最初に考えるべきは、コードではなくこういう問いです。

この「ユーザー」というものは、最低限どんな情報を必ず持っているべきか。
逆に、「あってもなくてもいい情報」はどれか。
一度決めたら変わらない情報(IDなど)はどれか。
時間が経つと変わりうる情報(名前、ステータスなど)はどれか。

この問いに答えた結果を、TypeScript の型として落とし込んでいく——それが「実務のオブジェクト設計」の感覚です。

必須と任意(optional)をどう分けるかの感覚

「このオブジェクトが“成立するために”絶対必要なものは何か」

たとえば、ユーザーを表す型を考えます。

type User = {
  id: string;
  name: string;
  email?: string;
  iconUrl?: string;
};
TypeScript

ここで idname は必須、emailiconUrl は optional にしています。
これは単に「そう書いた」ではなく、こういう設計の意思が入っています。

id がないユーザーは存在しない。
name がないユーザーも存在しない。
でも、email やアイコン画像は、登録していないユーザーもいる。

実務では、「このオブジェクトが“有効な状態”とみなされるために、最低限必要な情報は何か」をまず決めます。
それが必須プロパティになります。

逆に、「あとから埋まるかもしれない」「なくても動く」ものは optional にします。
ここを曖昧にして全部 optional にしてしまうと、「何が必須で何が任意なのか」がコードから読み取れなくなり、バグの温床になります。

「null と undefined と optional」をどう使い分けるか

実務では、?(optional)だけでなく、null を使うかどうかも設計の一部です。

type UserProfile = {
  bio?: string;          // そもそもプロフィールが未設定
  birthday: string | null; // 誕生日という項目はあるが、未入力の場合は null
};
TypeScript

ここには、微妙だけど大事な違いがあります。

bio?: string は、「プロフィール文という概念自体が存在しない状態もありうる」。
birthday: string | null は、「誕生日という項目は必ず存在するが、値が入っていないことがある」。

実務では、「その項目が“存在しない”のか、“存在するが空なのか”」を区別したい場面がよくあります。
そのときに、optional と null の使い分けが効いてきます。

ネスト構造は「意味のある塊ごとに型を切る」

住所・プロフィール・メタ情報などを「塊」として扱う

たとえば、こんなユーザー情報があるとします。

const user = {
  id: "u_1",
  name: "Taro",
  address: {
    country: "Japan",
    city: "Tokyo",
    zip: "100-0001",
  },
  profile: {
    bio: "Hello",
    website: "https://example.com",
  },
  meta: {
    createdAt: "2025-01-01T00:00:00Z",
    updatedAt: "2025-01-02T00:00:00Z",
  },
};
TypeScript

これをそのまま1つの型で書くこともできますが、実務ではだいたいこう分けます。

type Address = {
  country: string;
  city: string;
  zip: string;
};

type Profile = {
  bio?: string;
  website?: string;
};

type Meta = {
  createdAt: string;
  updatedAt: string;
};

type User = {
  id: string;
  name: string;
  address: Address;
  profile: Profile;
  meta: Meta;
};
TypeScript

ここでやっているのは、「意味のある塊ごとに名前をつけている」ことです。

住所は住所として Address。
プロフィールはプロフィールとして Profile。
メタ情報は Meta。

こうしておくと、

User 以外の型でも Address を再利用できる。
Meta を持つ別のエンティティ(記事、コメントなど)にも同じ Meta 型を使える。
「このプロパティは何を表しているのか」が型名から伝わる。

実務の感覚としては、「ネストが深くなってきたら、“この塊には名前をつけられるか?”と自分に問う」のがコツです。
名前をつけられるなら、それは型として切り出すタイミングです。

「APIレスポンスそのまま」と「アプリ内で使う型」を分ける

よくあるのが、API からこういうレスポンスが返ってくるパターンです。

{
  "id": "u_1",
  "name": "Taro",
  "address_country": "Japan",
  "address_city": "Tokyo",
  "created_at": "2025-01-01T00:00:00Z"
}

これをそのまま型にすると、こうなります。

type UserResponse = {
  id: string;
  name: string;
  address_country: string;
  address_city: string;
  created_at: string;
};
TypeScript

でも、アプリの中ではこう扱いたくなります。

type User = {
  id: string;
  name: string;
  address: {
    country: string;
    city: string;
  };
  createdAt: Date;
};
TypeScript

実務では、「外から来る形(APIレスポンス)」と「アプリ内で扱いやすい形(ドメインモデル)」を分けるのが普通です。
そして、「UserResponse → User」に変換する関数を用意します。

function toUser(res: UserResponse): User {
  return {
    id: res.id,
    name: res.name,
    address: {
      country: res.address_country,
      city: res.address_city,
    },
    createdAt: new Date(res.created_at),
  };
}
TypeScript

この設計の感覚はとても大事で、
「外部の都合(APIの都合)にアプリ内部の型を引きずられない」ようにするための防御線です。

不変なものと変わりうるものを分ける(readonly の感覚)

「一度決めたら変えない」ものを型でロックする

実務でよく出てくるのが、「ID や作成日時は絶対に変わらない」という前提です。

type User = {
  readonly id: string;
  name: string;
  readonly createdAt: string;
};
TypeScript

ここで readonly を付けるのは、単に「そういう文法があるから」ではなく、
「このプロパティはビジネス的に不変だ」という意思表示です。

実務の感覚としては、こういう問いを立てます。

この値は、時間が経っても変わりうるか?
変わるとしたら、誰が、どんな操作で変えるのか?
逆に、「変わったら困る」ものはどれか?

「変わったら困る」ものには readonly を付けておくと、
うっかり更新しようとしたときに TypeScript が止めてくれます。

function updateName(user: User, newName: string) {
  user.name = newName;      // OK
  // user.id = "other";     // エラー
}
TypeScript

「ビジネスルールを型に刻む」という感覚が持てると、readonly はただの修飾子ではなくなります。

「部分的な更新」と「全体の形」をどう両立させるか

Partial と「更新用の型」の考え方

実務では、「User 全体」ではなく「一部だけ更新する」ことがよくあります。

type User = {
  id: string;
  name: string;
  email?: string;
  iconUrl?: string;
};
TypeScript

たとえば、「プロフィール編集画面」では、nameiconUrl だけ更新するかもしれません。
そのときに、更新用の型をこう定義することがあります。

type UserUpdate = Partial<Pick<User, "name" | "email" | "iconUrl">>;
TypeScript

これは、「User の name / email / iconUrl を全部 optional にした型」です。
実務の感覚としては、

User は「完全な姿」を表す型。
UserUpdate は「更新リクエスト」を表す型。

という役割分担になります。

そして、更新関数はこうなります。

function updateUser(user: User, patch: UserUpdate): User {
  return { ...user, ...patch };
}
TypeScript

ここで守られているのは、

patch に User に存在しないプロパティは渡せない。
各プロパティの型は User と一致していないといけない。

ということです。

「完全な形」と「部分的な更新」を別の型として設計する
この感覚が持てると、オブジェクト設計は一気に実務寄りになります。

「設計感覚」を育てるために、常に自分に問うべきこと

これは“何のオブジェクト”なのかを言葉にできるか

User、Article、Order、Product……
型に名前をつけるとき、「この型は何を表しているのか」を一言で説明できるかを自分に問います。

「APIレスポンスとしてのユーザー」なのか。
「アプリ内部で扱うユーザー」なのか。
「DBに保存されるユーザー」なのか。

それぞれ役割が違うなら、型も分けた方がいい。
実務では、「同じ“ユーザー”でも、文脈ごとに別の型があっていい」という感覚が大事です。

このプロパティは「必須」「任意」「不変」「変わりうる」のどれか

プロパティを1つ追加するときに、こう自分に問いかけます。

これは、このオブジェクトが成立するために必須か?
あとから埋まるかもしれない任意の情報か?
一度決めたら変えない不変の情報か?
時間とともに変わりうる情報か?

その答えが、必須 / ? / readonly の選択に直結します。
「なんとなく全部 string で optional」ではなく、「意味に応じて型を選ぶ」のが、実務のオブジェクト設計です。

このネストは「名前をつけられる塊」かどうか

ネストが増えてきたら、こう考えます。

この部分は「住所」と呼べるか?
「プロフィール」と呼べるか?
「メタ情報」と呼べるか?

名前をつけられるなら、それは型として切り出すサインです。
そうすると、再利用できるし、テストもしやすくなるし、コードを読む人にも優しい。


最後に:文法の次に来るのは「意味を型に刻む」感覚

ここまで話してきたことを一言でまとめると、
実務でのオブジェクト設計は、「ビジネスの意味を TypeScript の型に刻んでいく作業」です。

必須か、任意か。
不変か、変わりうるか。
外部の形か、内部の形か。
完全な姿か、部分的な更新か。

これらを一つひとつ意識して型に落としていくと、
TypeScript はただの「型チェックツール」ではなく、
「仕様書をそのままコードにしたような、強力な設計の支え」になってくれます。

文法はもうかなり押さえられているので、
これからはぜひ、「このプロパティ、本当はどういう意味を持っているんだっけ?」と自分に問いながら型を書いてみてください。
その問いの質が、そのままオブジェクト設計の質になります。

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