1. ホーム
  2. TypeScript

【TypeScript】オプショナルプロパティ(?)の使い方|省略可能なプロパティと undefined の扱いを解説

Share

TypeScript でオブジェクトの型を書いていると、「この値はあるときとないときがある」という場面に必ず出会います。そこで使うのがプロパティ名のあとに ? を付けるオプショナルプロパティです。書き方自体は age?: number と1文字足すだけなのですが、実際に使い始めると「読み取ると number | undefined になって毎回チェックさせられる」「| undefined と書くのと何が違うのか」「Partial<T> との使い分けが分からない」といった疑問が出てきます。この記事では ? の基本から、値がないときの扱い方、オプショナル引数や Partial<T> との関係、exactOptionalPropertyTypes による挙動の変化までを順に整理します。

? を付けたプロパティは省略できる

オプショナルプロパティは、プロパティ名の直後に ? を書くだけで定義できます。? が付いたプロパティはオブジェクトを作るときに書かなくてよいようになり、書かなくても「型が足りない」というエラーになりません。逆に ? の付いていないプロパティは必須で、省略するとエラーになります。

user.ts
interface User {
  name: string;   // 必須
  age?: number;   // 省略してよい
}

// age を書かなくても OK
const alice: User = { name: 'Alice' };

// もちろん書いてもよい
const bob: User = { name: 'Bob', age: 30 };

// name は必須なので、これはエラー
// Property 'name' is missing in type '{ age: number; }' but required in type 'User'.
const carol: User = { age: 20 };

この記法は interface でも type でもクラスのプロパティでも同じように使えます。メソッドの場合は greet?(): void のように名前のあとに ? を付けます。API のレスポンスのように「値が返ってくるとは限らないフィールド」や、設定オブジェクトのように「指定しなければ既定値で動くオプション」を表すのが主な用途です。

readonly と組み合わせる

?readonly と併用できます。書く順番は readonly が先、? がプロパティ名の直後です。「あとから書き換えられないが、そもそも無くてもよい」プロパティを表現できます。

config.ts
interface Config {
  readonly id?: string;   // readonly → 名前 → ? の順
  readonly retry?: number;
}

const config: Config = {};        // 省略できる
// config.id = 'abc';             // Cannot assign to 'id' because it is a read-only property.

読み取ると number | undefined になる

? を付けると、そのプロパティを読み取ったときの型は 元の型に undefined を足したユニオン型になります。age?: number なら user.age の型は number | undefined です。プロパティが存在しないオブジェクトから読み取れば undefined が返ってくるので、これは JavaScript の実際の挙動をそのまま型にしたものだと考えると分かりやすいでしょう。

そのため、strictNullChecks が有効な環境ではそのまま number として扱おうとするとエラーになります。

read.ts
interface User {
  name: string;
  age?: number;
}

const user: User = { name: 'Alice' };

const age = user.age;          // age: number | undefined

// error TS18048: 'user.age' is possibly 'undefined'.
console.log(user.age.toFixed(0));

// error TS2322: Type 'number | undefined' is not assignable to type 'number'.
const n: number = user.age;

毎回チェックを要求されるのは煩わしく感じますが、これは「値がないかもしれない場所を見落としていませんか」という警告です。ここで手を抜かずに次のいずれかの方法で undefined を潰しておくと、実行時の Cannot read properties of undefined をコンパイル時に防げます。

値がないときの扱い方

undefined かどうか確かめて絞り込む

もっとも素直なのは、ifundefined でないことを確認してから使う方法です。TypeScript の型の絞り込み(Narrowing)が働き、if の中では number として扱えるようになります。

narrow.ts
function describe(user: User): string {
  if (user.age !== undefined) {
    // ここでは user.age: number
    return `${user.name}(${user.age.toFixed(0)}歳)`;
  }
  return `${user.name}(年齢不明)`;
}

ここで if (user.age) と真偽値で判定してしまうと、age0 のときも「無し」と同じ扱いになってしまいます。数値や文字列を持つオプショナルプロパティでは、0 や空文字と undefined を混同しないよう !== undefined で明示的に比較するのが安全です。

?. と ?? を組み合わせる

オプショナルチェイニング(?.)は、左側が null または undefined ならその場で評価を打ち切って undefined を返す演算子です。Null 合体演算子(??)は左側が nullundefined のときだけ右側の値を使います。この2つを組み合わせると、ネストしたオプショナルプロパティも1行で書けます。

chaining.ts
interface Profile {
  bio?: string;
  address?: {
    city?: string;
  };
}

interface User {
  name: string;
  age?: number;
  profile?: Profile;
  greet?(): void;
}

const user: User = { name: 'Alice' };

// ?. で途中が無ければ undefined になる
const city = user.profile?.address?.city;   // city: string | undefined

// ?? で既定値を与えれば string になる
const label: string = user.profile?.address?.city ?? '未設定';

// 数値も同様(0 は 0 のまま残る)
const age: number = user.age ?? 0;

// オプショナルメソッドは ?.() で「あれば呼ぶ」
user.greet?.();

注意したいのは ??|| の違いです。|| は左側が「falsy」なら右側を返すため、0・空文字・false でも既定値に置き換わってしまいます。オプショナルプロパティの既定値には ?? を使ってください。

分割代入のデフォルト値でまとめて埋める

設定オブジェクトのように省略可能なプロパティが複数あるときは、分割代入のデフォルト値がいちばん簡潔です。デフォルト値はプロパティの値が undefined のときに適用されるので、プロパティを省略した場合はもちろん、明示的に undefined を渡した場合でも既定値が使われます。

defaults.ts
interface FetchOptions {
  url: string;
  method?: 'GET' | 'POST';
  timeout?: number;
  retry?: number;
}

function request(options: FetchOptions) {
  // 分割代入で受け取りつつ既定値を与える
  const { url, method = 'GET', timeout = 5000, retry = 0 } = options;

  // ここから先はすべて undefined を含まない型
  // method: 'GET' | 'POST' / timeout: number / retry: number
  console.log(url, method, timeout, retry);
}

request({ url: '/api/posts' });                       // GET 5000 0
request({ url: '/api/posts', timeout: undefined });   // これも 5000 になる

関数の入口でこうして既定値を確定させてしまえば、処理の本体では undefined のことを一切考えなくてよくなります。オプショナルプロパティを扱う関数は、この「境界で埋めて中では気にしない」という形にしておくと読みやすくなります。

? と | undefined は同じではない

読み取ったときの型が同じ number | undefined になるため、age?: numberage: number | undefined は同じもののように見えます。しかしプロパティを省略できるかどうかが違います? は「書かなくてよい」ことを表しますが、| undefined はあくまで型がユニオンになっただけなので、プロパティ自体は必須のままです。

compare.ts
interface A {
  age?: number;             // 省略できる
}

interface B {
  age: number | undefined;  // 必ず書く必要がある
}

const a: A = {};            // OK

// error TS2741: Property 'age' is missing in type '{}' but required in type 'B'.
const b: B = {};

const b2: B = { age: undefined };   // こう書けば OK
書き方プロパティの省略明示的な undefined の代入読み取ったときの型
age?: numberできるできる(既定の設定)number | undefined
age: number | undefinedできないできるnumber | undefined
age?: number | undefinedできるできるnumber | undefined

| undefined のほうは「値が無いことを意識して明示させたい」ときに役立ちます。たとえばフォームの入力値をまとめた型で email: string | undefined と書いておけば、email の設定を書き忘れたコードがコンパイルエラーになります。単に「省略してもよいオプション」を表したいなら ?、「値が無い状態も含めて必ず扱ってほしい」なら | undefined と考えると選びやすいはずです。

オプショナル引数(arg?: T)との関係

関数の引数にも ? を付けられます。arg?: T と書くと呼び出し時にその引数を省略でき、関数の中での型は T | undefined になります。考え方はオプショナルプロパティと同じですが、引数には並び順の制約と、デフォルト値との排他という2つのルールがあります。

params.ts
function greet(name: string, title?: string): string {
  // title: string | undefined
  return title ? `${title} ${name}` : name;
}

greet('Alice');              // 省略できる
greet('Alice', 'Dr.');
greet('Alice', undefined);   // 明示的な undefined も渡せる

// error TS1016: A required parameter cannot follow an optional parameter.
function bad(title?: string, name: string) {}

// error TS1015: Parameter cannot have question mark and initializer.
function bad2(title?: string = 'Dr.') {}

// デフォルト値を付けると、省略可能かつ中では undefined を含まない型になる
function good(name: string, title: string = 'Dr.'): string {
  // title: string(undefined にならない)
  return `${title} ${name}`;
}

オプショナル引数は「後ろから省略していく」ものなので、必須の引数をオプショナル引数より後ろに置けません。省略できる項目が増えてきたら、引数を並べるのをやめてオプショナルプロパティを持つ1つのオブジェクトにまとめるほうが扱いやすくなります。呼び出し側でどの値を渡しているかが名前で分かり、並び順にも縛られません。

なお、? とデフォルト値は同時に書けませんが、目的が「省略されたら既定値を使う」ことなら title: string = 'Dr.' と書けば十分です。この形なら呼び出し側では省略可能になり、関数の中では string として扱えます。

Partial<T> との違いと関係

Partial<T> はユーティリティ型のひとつで、既存の型のすべてのプロパティに ? を付けた型を作ります。実装は次のようなマップ型で、やっていることは「全プロパティをオプショナルプロパティにする」だけです。つまり Partial? の対抗手段ではなく、? を機械的に付けるための道具です。

partial.ts
// lib.es5.d.ts の定義
// type Partial<T> = { [P in keyof T]?: T[P] };

interface User {
  name: string;
  email: string;
  age: number;
}

// { name?: string; email?: string; age?: number }
type UserPatch = Partial<User>;

// 更新したい項目だけ渡せる関数
function updateUser(id: number, patch: UserPatch) {
  // ...
}

updateUser(1, { age: 31 });
updateUser(1, {});   // 何も渡さなくても型としては通る

// 逆向きの変換もある(? を外して必須にする)
type StrictUser = Required<UserPatch>;   // { name: string; email: string; age: number }

使い分けは単純で、一部のプロパティだけ省略可能なら型定義に直接 ? を書き、元の型の全プロパティを一括で省略可能にしたいなら Partial<T> を使うと考えれば十分です。「更新用のパッチ型」や「部分的な設定の上書き」のように、必須の型が先にあってそれを緩める場面が Partial の出番です。

ひとつ気をつけたいのは、Partial<T>いちばん外側のプロパティにしか ? を付けないことです。入れ子になったオブジェクトの中身は必須のまま残るので、次のようなコードはエラーになります。再帰的に緩めたいときは自分で再帰的なマップ型を書く必要があります。

partial-nested.ts
type Settings = {
  theme: { color: string; size: number };
};

// theme は省略できるが、書くなら color と size は必須
// error TS2741: Property 'color' is missing in type '{ size: number; }'...
const patch: Partial<Settings> = { theme: { size: 14 } };

const ok: Partial<Settings> = {};   // これは OK

また、Partial<T> は元の型の readonly をそのまま引き継ぎます。readonly a: string を持つ型に Partial を掛けると readonly a?: string になり、代入しようとすれば Cannot assign to 'a' because it is a read-only property. というエラーが出ます。? だけが追加され、他の修飾子は保たれると覚えておけば混乱しません。

exactOptionalPropertyTypes で「省略」と「undefined」を区別する

既定の設定では、age?: number というオプショナルプロパティに age: undefined と明示的に代入することが許されています。「プロパティが無い」と「プロパティはあるが値が undefined」を型が区別していない、ということです。これを厳密に区別させるのが tsconfig.jsonexactOptionalPropertyTypes です。

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true
  }
}

この設定を有効にすると、age?: number は「省略してよいが、書くなら number でなければならない」という意味になります。undefined を代入したいなら、型のほうに | undefined を足して age?: number | undefined と書く必要があります。

exact.ts
interface User {
  name: string;
  age?: number;
}

const a: User = { name: 'Alice' };   // 省略は OK

// error TS2375: Type '{ name: string; age: undefined; }' is not assignable to type 'User'
// with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of
// the target's properties.
const b: User = { name: 'Bob', age: undefined };

interface User2 {
  name: string;
  age?: number | undefined;   // 明示的な undefined も許す
}

const c: User2 = { name: 'Carol', age: undefined };   // OK

// 読み取ったときの型は undefined を含んだまま(省略されている可能性があるため)
const age = a.age;   // age: number | undefined

// オプショナルプロパティなので delete できる
delete a.age;

ここで見落としやすいのは、読み取り側の型は変わらないという点です。プロパティが省略されている可能性は残るので、a.ageexactOptionalPropertyTypes を有効にしても number | undefined のままです。この設定が効くのは代入する側だけだと理解しておいてください。

また、この設定はプロパティに対するものなので、オプショナル引数(title?: string)には影響しません。exactOptionalPropertyTypes を有効にした状態でも greet('Alice', undefined) は通ります。Partial<T> のほうはプロパティに ? を付ける型なので影響を受け、Partial<{ a: string }>{ a: undefined } を代入するとエラーになります。

ランタイムでは in と Object.keys に差が出る

? は型の情報なのでコンパイル後の JavaScript には残りません。残るのは「プロパティが実際に存在するかどうか」だけです。そしてプロパティを省略した場合と、明示的に undefined を代入した場合は、実行時に区別できますin 演算子や Object.keys() の結果が変わるからです。

runtime.ts
const omitted = { name: 'Alice' };
const explicit = { name: 'Bob', age: undefined };

console.log('age' in omitted);          // false
console.log('age' in explicit);         // true

console.log(Object.keys(omitted));      // [ 'name' ]
console.log(Object.keys(explicit));     // [ 'name', 'age' ]

console.log(JSON.stringify(omitted));   // {"name":"Alice"}
console.log(JSON.stringify(explicit));  // {"name":"Bob"}  ← undefined は消える

// プロパティを読むだけならどちらも undefined
console.log(omitted.age === undefined, explicit.age === undefined);   // true true

この違いが実害になりやすいのがスプレッド構文による既定値の上書きです。スプレッドは存在するプロパティを値ごとコピーするため、undefined が入ったプロパティは既定値を undefined で塗り潰してしまいます。exactOptionalPropertyTypes が「明示的な undefined」を止めてくれるのは、まさにこういう事故を防ぐためです。

spread.ts
const defaults = { age: 20 };

// プロパティが無いので既定値が残る
console.log({ ...defaults, ...{ name: 'Alice' } });
// { age: 20, name: 'Alice' }

// 明示的な undefined が既定値を上書きしてしまう
console.log({ ...defaults, ...{ name: 'Bob', age: undefined } });
// { age: undefined, name: 'Bob' }

// 分割代入のデフォルト値なら undefined でも既定値が使われる
const { age = 20 } = { age: undefined };
console.log(age);   // 20

もうひとつ知っておきたいのが、in 演算子ではオプショナルプロパティの型を絞り込めないことです。'age' in usertrue でも、プロパティの値が undefined である可能性は残っているため、TypeScript は number | undefined のままにします。存在確認ではなく値の確認をしたいのですから、絞り込みには user.age !== undefined を使ってください。

in-operator.ts
interface User {
  name: string;
  age?: number;
}

function f(user: User) {
  if ('age' in user) {
    // error TS2322: Type 'number | undefined' is not assignable to type 'number'.
    const n: number = user.age;
  }

  if (user.age !== undefined) {
    const n: number = user.age;   // OK
  }
}

思ったようにチェックされないとき

strictNullChecks が無効だと ? の意味が半分になる

strictNullChecks が無効な環境では、undefined がどんな型にも代入できるものとして扱われます。そのため number | undefinednumber の区別が事実上なくなり、? は「プロパティを省略してよい」という意味だけになります。次のコードは strictNullChecks が無効だと1つもエラーになりません。

no-strict.ts
interface User {
  name: string;
  age?: number;
}

const user: User = { name: 'Alice' };

// strictNullChecks が無効だと、どちらもエラーにならない
const n: number = user.age;
console.log(user.age.toFixed(0));   // 実行時に TypeError になる

オプショナルプロパティの一番の価値は「値が無いケースを忘れていないか」をコンパイラに見張らせることなので、この設定が無効だと恩恵の大半を失います。tsconfig.json"strict": truestrictNullChecks を含む)を有効にしておくのが前提だと考えてください。

The operand of a ‘delete’ operator must be optional. と言われる

delete user.name のように ? の付いていないプロパティを削除しようとすると、TS2790 のこのエラーが出ます。必須と宣言したプロパティを消してしまうと、その型が保証している内容が壊れてしまうためです。削除する必要があるなら、そのプロパティは最初から ? を付けてオプショナルにしておくべきだ、という指摘だと受け取ってください。

絞り込んだのに undefined が消えない

if (user.age !== undefined) で絞り込んだのに、その中で user.age がまた number | undefined に戻ってしまうことがあります。原因は、絞り込みの内側でコールバック関数やアロー関数を書いていることです。関数はいつ実行されるか分からず、それまでにプロパティが書き換えられる可能性を否定できないため、TypeScript は関数の中に入った時点で絞り込みを捨てます。

対策は、絞り込む前に値を const のローカル変数へ取り出しておくことです。const は再代入されないと保証されているので、関数の中でも絞り込みが維持されます。

callback.ts
function ng(user: User, items: number[]) {
  if (user.age !== undefined) {
    // コールバックの中では絞り込みが解除される
    // error TS2322: Type 'number | undefined' is not assignable to type 'number'.
    items.forEach(() => {
      const n: number = user.age;
    });
  }
}

function ok(user: User, items: number[]) {
  const age = user.age;          // 先に const で受ける
  if (age !== undefined) {
    items.forEach(() => {
      const n: number = age;     // OK
    });
  }
}

なお、同じスコープ内でふつうに関数を呼ぶだけなら絞り込みは維持されるので、あらゆる関数呼び出しで解除されるわけではありません。解除されるのは絞り込みの内側で新しい関数を定義した場合です。オプショナルプロパティを何度も参照する処理では、最初にローカル変数へ移してしまうのが確実です。

まとめ

プロパティ名のあとに ? を付けると、そのプロパティは省略可能になり、読み取ったときの型は T | undefined になります。使うときは !== undefined での絞り込み、?.?? の組み合わせ、分割代入のデフォルト値のいずれかで undefined を先に潰しておくのが基本です。?| undefined の違いは「省略できるか」で、? は省略を許し、| undefined は必ず書かせます。関数の引数でも同じ記法が使えますが、必須引数を後ろに置けない点と、デフォルト値と併記できない点だけ注意してください。全プロパティをまとめて省略可能にしたいときは Partial<T> が便利で、これは ? を機械的に付けるマップ型です(入れ子には及ばず、readonly は引き継ぎます)。さらに exactOptionalPropertyTypes を有効にすると「省略」と「明示的な undefined」が区別され、スプレッドで既定値を潰すような事故を防げます。最後に、これらの恩恵は strictNullChecks が有効であることが前提です。

参考ページ