【TypeScript】オプショナルプロパティ(?)とreadonlyの使い方

JavaScript

こんにちは、かつコーチです。

前回は、enum(列挙型)の使い方について解説しました。

今回は、オブジェクトの型定義をより実務向きにする2つの記法、「オプショナルプロパティ(?)」と「readonly」を扱います。

どちらも小さな記号ひとつですが、実務で型定義をする際にほぼ必ずお世話になる存在です。

オプショナルプロパティとは?

プロパティを「あってもなくてもいい」ものにする

interfacetype でオブジェクトの形を定義するとき、通常はすべてのプロパティが必須になります。

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 な配列は、pushpop のように配列そのものを変更するメソッドが使えなくなります。

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 で予防できる
  • 「変わるべきか、変わらないべきか」を意識してつけるかどうかを判断する

次に読むべき記事

次回は、nullundefined を安全に扱うための strictNullChecks について解説します。

→ 次の記事:nullとundefinedの安全な扱い方(strictNullChecks)

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