JavaScript / TypeScript公開 約9分

TypeScript オプショナルプロパティで省略と値の削除を分ける

編集しなかった項目と、値を消した項目を同じ扱いにしないために。表示名の部分更新を例に、疑問符・null・undefinedの役割とexactOptionalPropertyTypesの設定を型検査と実行で確かめます。

この記事の目次
  1. 三つの編集指示を、先に言葉で決める
  2. undefinedを渡す余地は、設定で変わる
  3. 部分更新を一つの関数へ閉じ込める
  4. 消したいのに残る処理と、省略できない型
  5. 型の約束と、受信データの検査を分ける

自己紹介の表示名を編集する関数で、変更しなかった項目まで空になってしまう。「項目が来なければ今の値を残す」「nullなら値を消す」と決めれば、部分更新の意味を分けられます。TypeScriptのオプショナルプロパティは、その約束を型へ書くために使えます。

題材は架空のプロフィールです。オブジェクトとif文、基本的な型注釈が分かる人向けに、通信を行わない更新関数を作ります。Node.js 24系と、比較用に固定したTypeScript 5.9.3を使います。保存先やフォームを作る前に、渡すデータの意味を確かめる練習です。

三つの編集指示を、先に言葉で決める

現在の表示名を「山のノート」とします。更新データにnicknameがなければそのまま、文字列があれば置き換え、nullがあれば未設定にします。nullに削除の意味が自動で備わっているわけではありません。この関数の契約として決める意味です。

更新データ 指示 更新後の表示名
{} 触らない 山のノート
{ nickname: "川のノート" } 置き換える 川のノート
{ nickname: null } 消す 未設定

この表を型にするとnickname?: string | nullです。疑問符は項目の省略を許し、stringとnullは、項目があるときの値を表します。公式Handbookのオブジェクト型では、疑問符を付けた項目を省略して渡せることが説明されています。

比較としてnickname: string | undefinedには疑問符がありません。undefinedという値を渡せても、項目そのものは必要です。この二つを「どちらも値がない場合がある」でまとめると、更新指示の有無を表せなくなります。

undefinedを渡す余地は、設定で変わる

exactOptionalPropertyTypesを有効にすると、疑問符で省略を許した項目へ、宣言していないundefinedを値として書けなくなります。公式の設定資料には、項目がないことと、項目があって値がundefinedであることの違いが示されています。

strictだけではこの設定は有効になりません。今回のコマンドでは両方を指定します。既存プロジェクトへ導入する場合は、設定を切り替える前に、現在undefinedを渡している場所が「省略」の意図なのか、本当に値として必要なのかを調べます。

また、書き込みが厳密になっても、省略した項目を読むとundefinedになる可能性は残ります。patch.nicknameを読むだけでは、更新指示があるかを判断したことになりません。今回は先にin演算子で項目の存在を調べます。扱うのは、このコードで作る普通のオブジェクトです。

部分更新を一つの関数へ閉じ込める

空の練習フォルダで次を実行します。依存関係は練習フォルダ内へ入ります。

npm init -y
npm install --save-dev --save-exact typescript@5.9.3

次をprofile.tsへ保存します。元データを書き換えず、毎回新しいプロフィールを返す形です。表示名がnullなら案内文へ変えますが、内部の値までその案内文へ置き換えません。

type Profile = { nickname: string | null };
type ProfilePatch = { nickname?: string | null };

function applyPatch(current: Profile, patch: ProfilePatch): Profile {
  if (!("nickname" in patch)) return { ...current };
  return { ...current, nickname: patch.nickname };
}

function show(profile: Profile): string {
  return profile.nickname === null ? "未設定" : profile.nickname;
}

const original: Profile = { nickname: "山のノート" };
const changes: ProfilePatch[] = [
  {},
  { nickname: "川のノート" },
  { nickname: null }
];
for (const patch of changes) {
  console.log(show(applyPatch(original, patch)));
}
console.log("元データ:" + show(original));

型検査と変換に成功してから、出力されたJavaScriptを実行します。型エラーの後に以前の出力を実行してしまわないよう、成功したことを確認して二行目へ進みます。

npx tsc profile.ts --strict --exactOptionalPropertyTypes --target ES2022 --module commonjs --noEmitOnError --outDir out
node out/profile.js

結果は順に「山のノート」「川のノート」「未設定」「元データ:山のノート」です。三件ともoriginalから更新しているため、一件目の結果を次へ引き継いでいません。連続更新を試すなら、返されたProfileを次回のcurrentとして渡します。

この設定では、項目の存在を調べた後のnicknameはstringかnullに絞られます。完成例からexactOptionalPropertyTypesを外すと、明示的なundefinedも来得るため、戻り値のnicknameに関する型エラーになります。エラーをasで隠さず、想定した設定で検査しているかを先に確認してください。

消したいのに残る処理と、省略できない型

一つ目の失敗は、更新部分をnickname: patch.nickname ?? current.nicknameへ変えることです。nullとundefinedの両方で以前の値を選ぶため、明示的に消したい三件目も「山のノート」になります。型検査が通っても、表に書いた契約とは違う動作です。

修正は、完成例のように項目の有無で分岐し、存在する場合のnullをそのまま渡すことです。三件目が未設定へ戻ること、一件目は元の値を維持することを一組で確認します。未設定になったかだけを見ると、今度は省略まで削除扱いにした修正を見逃します。

二つ目は、疑問符を外し、type RequiredPatch = { nickname: string | undefined }とすることです。この型へ{}を代入するとTS2741となり、nicknameがないことを指摘されます。値にundefinedを許すことと、項目を省略できることは別だからです。部分更新なら疑問符を付けた契約へ戻します。

拒否される値を確かめるため、次を別ファイルrejected.tsへ保存します。完成用のprofile.tsには混ぜません。

type OptionalPatch = { nickname?: string | null };
const ambiguous: OptionalPatch = { nickname: undefined };

type RequiredPatch = { nickname: string | undefined };
const missing: RequiredPatch = {};

同じ厳密さで、出力を作らず検査します。

npx tsc rejected.ts --strict --exactOptionalPropertyTypes --noEmit

期待する診断は、最初が明示的なundefinedを拒否するTS2375、次が必須項目の欠落を示すTS2741です。このファイルの検査失敗は意図した結果です。前者は項目を省略するかnullにするかを選び、後者は項目を必須にしたかったのかを見直します。

型の約束と、受信データの検査を分ける

この関数は、自分のTypeScriptコードで作った更新指示を対象にしています。JSONを受け取って型を付けただけでは、文字列以外の値を拒否できません。外部からの入力はunknownから検査する記事のように、実行時の確認を別に用意します。

inは継承された項目も調べる演算子です。今回はリテラルで作ったデータへ用途を限定しています。任意のオブジェクトやgetterまで受け付ける境界として、そのまま使う例ではありません。また空文字列はこの型では許可されます。空文字も削除とするなら、nullへ変換する規則を入口へ追加する必要があります。

更新の種類が増え、たとえば表示名の自動生成も選ぶなら、疑問符一つへ意味を詰め込まず、判別可能なユニオンで操作の種類を表す方法もあります。今回の三通りで足りるかが、型の形を選ぶ境目です。

確認記録には、正常な四行、rejected.tsの二つの診断、nullを渡したときに以前の値へ戻らないことを残します。JavaScriptの無料レッスンで関数を画面につなぐときも、まず入力表と出力が一致することを確かめてから、フォームの操作を加えてください。

TRY A LESSON

記事の内容を、課題で確かめる

JavaScriptでフォームを作る練習 →

JavaScriptの学習内容 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る