この記事の目次
自己紹介の表示名を編集する関数で、変更しなかった項目まで空になってしまう。「項目が来なければ今の値を残す」「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
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。