【実務・中級編】関数引数における「Exact Optional Property Types」の有効化と挙動 – TypeScript コア・型システムの基礎解析バイブル

【TypeScript】`exactOptionalPropertyTypes` を制す:undefined代入の罠をコンパイル時に断つ極限の型設計

コードレビューをしていて、次のようなコードに遭遇したことはないだろうか。

type UserConfig = {
name: string;
age?: number;
};

// 呼び出し側
const config: UserConfig = {
name: “Taro”,
age: undefined, // アレ……? 動くけど、これ本当に意図してる?
};

一見、何気ないコードに見えるかもしれない。しかし、コンパイラ内部の型評価とランタイムの挙動の不一致を見過ごしている典型例だ。

TypeScriptのデフォルト(`exactOptionalPropertyTypes` が無効な状態)では、オプショナルプロパティ(`?`)を持つオブジェクトに `undefined` を明示的に代入することが許容される。しかし、これは「プロパティが存在しない(Missing)」状態と、「プロパティは存在するが値がundefinedである」状態という、ランタイムにおける致命的な非対称性を生み出す。

今回は、tsconfigの隠れた名設定 `exactOptionalPropertyTypes` を有効化し、型システムとランタイムの乖離を完全にハックする方法を、テクニカルリードの視点からロジカルに解説しよう。

—

1. なぜデフォルトのオプショナルプロパティは危険なのか?

まずは、TypeScriptの型システムがデフォルトで抱える「構造的型付けの罠」をコンパイラの視点から暴く。

type Options = {
timeout?: number;
};

function initialize(opts: Options) {
// デフォルト値を設定したい場面
const finalTimeout = opts.timeout ?? 1000;
console.log(finalTimeout);
}

// パターンA: プロパティ自体がない
initialize({}); // 1000が出力される

// パターンB: undefinedを明示的に渡す
initialize({ timeout: undefined }); // 1000が出力される……あれ、同じじゃないか?

一見、パターンAもパターンBも結果は同じに見える。だが、実務でJSONシリアライズやAPIリクエスト、データベースの部分更新(`Partial`)を行った瞬間、牙を剥く。

import { updateDatabase } from ‘./db’;

// パターンBのオブジェクトをJSON.stringifyするとどうなるか?
console.log(JSON.stringify({ timeout: undefined }));
// 出力: ‘{“timeout”:undefined}’ (JSONとしては不正、あるいはキーが残る)
// あるいはフレームワークによってはキーが “undefined” という文字列になることもある

// データベースのPATCH更新において
// 「フィールドを更新しない(undefined)」と「フィールドをNULLで上書きする」を混同する原因になる

TypeScriptの型 `number | undefined` は、「その型の値として undefined が許容される」ことを意味する。しかし `?` は、「プロパティの存在そのものがオプショナルである」ことを意味する。この2つは本来、全く別の概念なのだ。

—

2. `exactOptionalPropertyTypes` の導入と型評価のメカニズム

この混沌に秩序をもたらすのが、`tsconfig.json` のこのフラグだ。

{
“compilerOptions”: {
“target”: “ESNext”,
“module”: “NodeNext”,
“strict”: true,
“exactOptionalPropertyTypes”: true // ← これを有効化する
}
}

このフラグを有効にした瞬間、TypeScriptコンパイラはオプショナルプロパティの型評価を厳格化する。

  • 無効時: `age?: number` は内部的に `age?: number | undefined` として評価される。
  • 有効時: `age?: number` は `undefined` の明示的な代入を厳格に拒絶する。代入できるのは、実値(この場合は `number`)のみか、プロパティ自体を省略することのどちらかになる。

コンパイルエラーの例

type Profile = {
bio?: string;
};

// 【NG】exactOptionalPropertyTypes: true の環境下ではコンパイルエラーになる
const p1: Profile = {
bio: undefined,
// エラー: Type ‘undefined’ is not assignable to type ‘string’ with ‘exactOptionalPropertyTypes: true’.
};

// 【OK】プロパティ自体を省略する
const p2: Profile = {};

// 【OK】どうしてもundefinedを許容したい場合は、明示的に union を組む
type ExplicitProfile = {
bio: string | undefined; // オプショナル(?)ではなく、必須だがundefinedを取れる
};
const p3: ExplicitProfile = {
bio: undefined, // これなら通る
};

この違いは極めて重要だ。「プロパティが存在しないこと」と「値がundefinedであること」を、型レベルで完全に区別できるようになる。

—

3. 実務で即効性のあるプロダクションコード設計

フロントエンドのコンポーネント設計や、APIクライアントのオプション設計において、このフラグがどのようにコードの堅牢性を高めるか、実践的なコードを見てみよう。

以下は、高度な非同期APIクライアントと、それに紐づくコンポーネントPropsの設計パターンだ。

// ==========================================
// 1. APIリクエスト型定義のモデリング
// ==========================================
export type UserUpdatePayload = {
readonly id: string;
username?: string;
email?: string;
// プロパティが存在しない =「変更しない」
// 値を明示的に渡したい場合は別途スキーマ設計する
};

/

  • 厳格なパッチリクエスト送信関数

/
export async function patchUser(payload: UserUpdatePayload): Promise {
// ‘exactOptionalPropertyTypes’ が有効なため、
// 呼び出し側でうっかり `username: undefined` を渡すバグがコンパイル時に防がれる。

// 余計なundefinedプロパティがJSONに含まれないため、綺麗なペイロードを構築できる
const response = await fetch(`/api/users/${payload.id}`, {
method: ‘PATCH’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error(`Failed to update user: ${response.statusText}`);
}
}

// ==========================================
// 2. フロントエンドコンポーネント設計での応用
// ==========================================
import React from ‘react’;

type ButtonVariant = ‘primary’ | ‘secondary’ | ‘danger’;

type ActionButtonProps = {
label: string;
onClick: () => void;
// アイコンは省略可能だが、もし渡すならReactNodeでなければならない
// undefinedを誤って渡してレンダリングバグるのを防ぐ
leftIcon?: React.ReactNode;
variant?: ButtonVariant;
};

export const ActionButton: React.FC = ({
label,
onClick,
leftIcon,
variant = ‘primary’, // デフォルト引数のフォールバック
}) => {
return (

);
};

この設計がもたらすメリット

1. APIペイロードのクリーン化: サーバーに対して無駄な `undefined` を含むJSONを送信することが物理的に不可能になり、バックエンドでのバリデーションエラーや予期せぬ挙動を防ぐ。
2. コンポーネントの予測可能性: 「値が入っていない」ことの理由が明確になり、不必要なオプショナルチェーン(`?.`)や防衛的コードを排除できる。

—

4. 移行時の注意点とテクニカルリードからの助言

既存の大規模コードベースで `exactOptionalPropertyTypes: true` を有効化する場合、数多くのコンパイルエラー(主にライブラリの型定義や既存の不統一なコード)に直面するはずだ。

1. サードパーティ製ライブラリの型との不整合

古い、あるいは厳密に型付けされていないサードパーティ製ライブラリの `Partial` やProps定義と衝突することがある。その場合は、型アサーションやモジュール augmentation で一時的に逃げるか、ラッパーレイヤーを一枚噛ませる設計にリファクタリングしよう。

2. 「設定値の欠落」と「明示的なリセット」のトレードオフ

もしドメイン要件として「フォームでユーザーが入力内容をクリアした際、データベースの既存値を `NULL`(あるいは `undefined`)で上書きしたい」というユースケースがある場合、オプショナルプロパティ(`?`)ではなく、明示的なUnion型を使うべきだ。

type FormState = {
// 「未入力(変更なし)」ではなく「値が存在しないことを明示する」必要がある場合
middleName: string | null;
};

型に意思を持たせる。これがTypeScriptアーキテクチャの神髄である。

—

結びにかえて

`exactOptionalPropertyTypes` は、単なるコンパイラの厳格化フラグではない。それは、「データが存在しないことのセマンティクス(意味論)」をコードベース全体で統一するための強力な武器である。

「動くからいいか」で放置された `undefined` は、いつの日かプロダクション環境で予期せぬバグを引き起こす。テクニカルリードとしてチームを率いるあなたなら、今すぐ `tsconfig.json` を開き、このフラグを有効化すべきだ。

型システムを極限まで信頼できるものに仕立て上げ、美しく堅牢なコードベースを築き上げよう。

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