実務でのオブジェクト設計は「現実世界のモデル化」から始まる
まず大前提として、実務でのオブジェクト設計は、「TypeScript の文法をどう書くか」より前に、「現実の世界をどう切り取るか」から始まります。
ユーザー、商品、注文、記事、コメント……扱うのはいつも「現実に存在する何か」です。
TypeScript のオブジェクト型は、その「現実の何か」の設計図です。
だから最初に考えるべきは、コードではなくこういう問いです。
この「ユーザー」というものは、最低限どんな情報を必ず持っているべきか。
逆に、「あってもなくてもいい情報」はどれか。
一度決めたら変わらない情報(IDなど)はどれか。
時間が経つと変わりうる情報(名前、ステータスなど)はどれか。
この問いに答えた結果を、TypeScript の型として落とし込んでいく——それが「実務のオブジェクト設計」の感覚です。
必須と任意(optional)をどう分けるかの感覚
「このオブジェクトが“成立するために”絶対必要なものは何か」
たとえば、ユーザーを表す型を考えます。
type User = {
id: string;
name: string;
email?: string;
iconUrl?: string;
};
TypeScriptここで id と name は必須、email と iconUrl は 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たとえば、「プロフィール編集画面」では、name と iconUrl だけ更新するかもしれません。
そのときに、更新用の型をこう定義することがあります。
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 はただの「型チェックツール」ではなく、
「仕様書をそのままコードにしたような、強力な設計の支え」になってくれます。
文法はもうかなり押さえられているので、
これからはぜひ、「このプロパティ、本当はどういう意味を持っているんだっけ?」と自分に問いながら型を書いてみてください。
その問いの質が、そのままオブジェクト設計の質になります。

