こんにちは、かつコーチです。
前回は、enum(列挙型)の使い方について解説しました。
今回は、オブジェクトの型定義をより実務向きにする2つの記法、「オプショナルプロパティ(?)」と「readonly」を扱います。
どちらも小さな記号ひとつですが、実務で型定義をする際にほぼ必ずお世話になる存在です。
オプショナルプロパティとは?
プロパティを「あってもなくてもいい」ものにする
interface や type でオブジェクトの形を定義するとき、通常はすべてのプロパティが必須になります。
interface User {
id: number;
name: string;
email: string;
}
// ❌ emailが足りないためエラーになる
const user: User = {
id: 1,
name: "かつコーチ",
};
しかし実際のデータには、「あるときもあれば、ないときもある」プロパティがよく存在します。
たとえば、ユーザーのプロフィールに「自己紹介文」があるかどうかは人によって違いますよね。
こういったプロパティは、プロパティ名の後ろに ? をつけることで オプショナルプロパティ(任意プロパティ) にできます。
interface User {
id: number;
name: string;
email: string;
bio?: string; // あってもなくてもいい
}
const user1: User = {
id: 1,
name: "かつコーチ",
email: "katsu@example.com",
};
const user2: User = {
id: 2,
name: "山田太郎",
email: "yamada@example.com",
bio: "フロントエンドエンジニアです",
};
user1 のように bio を省略してもエラーにならず、user2 のように指定してもよい、という柔軟な型定義ができます。
オプショナルプロパティの型はundefinedを含む
ここで注意したいのが、bio?: string は実は bio: string | undefined とほぼ同じ意味を持つという点です。
function printBio(user: User) {
// user.bioの型は string | undefined
console.log(user.bio.length); // ❌ エラー:undefinedかもしれない
}
bio が存在しない可能性がある以上、そのままプロパティにアクセスしようとするとエラーになります。
私が最初にこれをやってしまったとき、「さっきまで動いていたのに、bio を足しただけでエラーが増えた」と混乱した記憶があります。
安全に使うには、値が存在するかどうかをチェックしてからアクセスします。
function printBio(user: User) {
if (user.bio) {
console.log(user.bio.length); // ここまで来ればstring型として扱える
} else {
console.log("自己紹介文はありません");
}
}
readonlyとは?
一度設定したら変更できないプロパティ
readonly は、プロパティの前につけることで「一度設定したら再代入できない」という制約を加える記法です。
interface User {
readonly id: number;
name: string;
}
const user: User = {
id: 1,
name: "かつコーチ",
};
user.name = "田中太郎"; // OK:readonlyではないので変更できる
user.id = 2; // ❌ エラー:readonlyプロパティは変更できない
id のように「一度決まったら変わらないはずの値」に readonly をつけておくと、うっかり書き換えてしまうミスをコンパイル時に防げます。
配列やオブジェクトのreadonly
配列にも readonly を使うことができます。
const numbers: readonly number[] = [1, 2, 3];
numbers.push(4); // ❌ エラー:readonly配列にはpushできない
readonly な配列は、push や pop のように配列そのものを変更するメソッドが使えなくなります。
ReadonlyArray<T> という書き方でも同じ意味になります。
const numbers: ReadonlyArray<number> = [1, 2, 3];
Before/Afterで見る実践的な使い方
API レスポンスをうっかり書き換えてしまう問題
私が実際に業務でハマったのが、APIから取得したデータをそのまま加工しようとして、意図せず元のデータまで書き換えてしまったケースです。
❌ Before:readonlyをつけずに定義した結果、意図せず値が変わる
interface ApiResponse {
userId: number;
fetchedAt: string;
}
function logResponse(response: ApiResponse) {
// デバッグのつもりで書いたコードが元データを書き換えてしまう
response.fetchedAt = "デバッグ用の値";
console.log(response);
}
const response: ApiResponse = { userId: 1, fetchedAt: "2026-08-13" };
logResponse(response);
console.log(response.fetchedAt); // "デバッグ用の値"(意図せず書き換わっている)
デバッグのためだけに書いた1行が、呼び出し元のオブジェクトまで変えてしまうという事故です。
オブジェクトは参照渡しになるため、関数の中で変更すると呼び出し元にも影響してしまいます。
✅ After:readonlyをつけて意図しない変更をコンパイル時に防ぐ
interface ApiResponse {
readonly userId: number;
readonly fetchedAt: string;
}
function logResponse(response: ApiResponse) {
// response.fetchedAt = "デバッグ用の値"; // ❌ readonlyなのでコンパイルエラーになる
console.log(response);
}
readonly をつけておくことで、「このプロパティは変更されないはず」という意図がコードそのものに残ります。
書き換えようとした瞬間にコンパイルエラーになるため、実行時まで気づかないバグを未然に防げます。
オプショナルとreadonlyを組み合わせる
? と readonly は組み合わせて使うこともできます。
interface Config {
readonly apiKey: string;
readonly timeout?: number; // 任意かつ変更不可
}
const config: Config = {
apiKey: "xxxx-xxxx",
};
console.log(config.timeout); // undefined
timeout を省略しても構いませんが、一度値を設定すればそのプロパティは変更できません。
設定値(config)のような「基本は決め打ちだが、一部だけ省略や上書きを許したい」データ構造でよく使われる組み合わせです。
オプショナルとreadonlyを使うべき場面の目安
使い分けの判断軸
どのプロパティに ? や readonly をつけるべきか、迷ったときの判断軸を整理しました。
| 状況 | 使う記法 | 理由 |
|---|---|---|
| 値が存在しないケースがある | ?(オプショナル) | undefinedを型として明示できる |
| IDや作成日時など、一度決まったら変わらない値 | readonly | 意図しない再代入をコンパイル時に防げる |
| APIレスポンスなど外部から受け取る値 | readonly | 呼び出し元での書き換え事故を防げる |
| フォーム入力のように頻繁に更新される値 | どちらもつけない | 制約が逆に邪魔になる |
「とりあえず全部 readonly にする」というのも一つの考え方ですが、更新前提の値にまで readonly をつけると、かえってコードが書きにくくなります。
「このプロパティは変わるべきか、変わらないべきか」を意識しながらつけていくのがコツです。
まとめ
この記事のポイント
?をつけると、プロパティを「あってもなくてもいい」オプショナルにできる- オプショナルプロパティの型には自動的に
undefinedが含まれる readonlyをつけると、そのプロパティへの再代入をコンパイル時に防げる- オブジェクトの参照渡しによる意図しない書き換えは、
readonlyで予防できる - 「変わるべきか、変わらないべきか」を意識してつけるかどうかを判断する
次に読むべき記事
次回は、null と undefined を安全に扱うための strictNullChecks について解説します。
→ 次の記事:nullとundefinedの安全な扱い方(strictNullChecks)