TypeScript:オプショナルとnull安全
1学習の目的
- オプショナルプロパティで「あってもなくてもよい項目」を表現できるようになる。前章で「すべて必須」だった型の弱点を解消する。
- オプショナルチェーンと ?? を使い、値が無い場合も安全に処理できるようになる。JS07で悩んだ Cannot read properties of undefined が構造的に起きなくなる。
2基礎解説
| やりたいこと | 書き方 | 意味 |
|---|---|---|
| 省略可能な項目 | email?: string | 無くてもよい |
| 安全にアクセス | user.email?.toUpperCase() | 無ければ undefined |
| 既定値を指定 | user.email ?? "未登録" | null/undefined のとき |
| 入れ子も安全に | a?.b?.c | 途中で無ければ止まる |
- ? を付けると省略可能な項目になる。email?: string の型は実際には string | undefined で、そのままでは使えない。
- ?.(オプショナルチェーン)は、値が無ければそこで止まって undefined を返す。user.email?.toUpperCase() は、email が無ければエラーにならず undefined になる。
- ?? と || は違う。?? はnull と undefined のときだけ右側を返すが、|| は 0 や "" でも右側を返してしまう(JS03)。
- 入れ子でも安全に辿れる。company.address?.city なら、address が無い時点で止まるのでエラーにならない。
- この仕組みが無ければ、毎回 if で確認する必要があった。TypeScriptは「確認を忘れること」自体を防いでくれる。
現場使用例:任意入力のフォーム項目、APIレスポンスの省略可能なフィールド、設定ファイルの初期値、ユーザーのプロフィール情報。
// ? で省略可能にする
type User = {
id: number;
name: string;
email?: string; // 実際の型は string | undefined
};
const u1: User = { id: 1, name: "田中" }; // OK
const u2: User = { id: 2, name: "鈴木", email: "a@b.com" };
// ❌ そのままメソッドは呼べない
// console.log(u1.email.toUpperCase());
// → 'u1.email' is possibly 'undefined'.
// ✅ ?. なら安全(無ければ undefined が返る)
console.log(u1.email?.toUpperCase()); // undefined
console.log(u2.email?.toUpperCase()); // A@B.COM
// ✅ ?? で既定値を指定
console.log(u1.email ?? "未登録"); // 未登録
// 入れ子でも安全
type Company = {
name: string;
address?: { city: string; zip?: string };
};
const c: Company = { name: "A社" };
console.log(c.address?.city ?? "住所未登録"); // 住所未登録
// ⚠ ?? と || の違い
const count = 0;
console.log(count || "既定値"); // "既定値"(0は偽と見なされる)
console.log(count ?? "既定値"); // 0(正しい)
3基本ドリル(10問)
出力 == 田中
email?: string と書く。省略してもエラーにならない。
type User = {
id: number;
name: string;
email?: string;
};
const user: User = { id: 1, name: "田中" };
console.log(user.name);
? を付けた項目は書かなくてよい。TS07までは全項目が必須だったので、「まだ登録されていない情報」を表現できなかった。
A. string B. string | undefined C. string | null D. any 選択★☆☆無料
解答 == B
省略された場合、その値は何になるか。
B
省略可能な項目は undefined との union になる。だからそのままメソッドを呼ぶとエラーになる。
出力 == undefined
user.email?.toUpperCase() と書く。?. を使えばエラーにならない。
type User = {
id: number;
name: string;
email?: string;
};
const user: User = { id: 1, name: "田中" };
console.log(user.email?.toUpperCase());
?. は「無ければそこで止まる」。JavaScriptなら Cannot read properties of undefined で落ちていた場面が、静かに undefined を返す。
const email: string | undefined = undefined; console.log(email ____ "未登録");
出力 == 未登録
null と undefined のときだけ右側を返す演算子。疑問符2つ。
??
?? は null 合体演算子。|| と違い、0 や空文字を「値あり」として扱ってくれる。
A. 0 B. "既定値" C. undefined D. エラー 選択★☆☆無料
解答 == B
0 は真偽値としては偽と扱われる(JS03)。
B
これが || の罠。在庫0個や金額0円が「未設定」と誤判定される。既定値を指定するなら ?? を使う。
出力2行。未登録 / a@b.com
?? で既定値を指定する。for...of で2件を回してもよい。
type User = {
id: number;
name: string;
email?: string;
};
const users: User[] = [
{ id: 1, name: "田中" },
{ id: 2, name: "鈴木", email: "a@b.com" },
];
for (const user of users) {
console.log(user.email ?? "未登録");
}
?? を使えば1行で安全に書ける。if で分岐する必要がなく、意図も明確になる。
type Company = { name: string; address?: { city: string } };
const c: Company = { name: "A社" };
console.log(c.address____.city ?? "住所未登録");
出力 == 住所未登録
「無ければ止まる」を意味する記号。疑問符とピリオド。
?
?. は途中で止まって undefined を返す。address が無い時点で処理が止まるので、その先の city にアクセスしようとしてエラーになることがない。
A. エラーになる B. undefined が返る C. null が返る D. 空文字が返る 選択★★☆無料
解答 == B
オプショナルチェーンは途中で止まる。
B
どこで止まっても結果は undefined。深い入れ子でも、1つの ?? で既定値を指定できる。
出力 == 在庫:0個
stock?: number とし、stock ?? 0 で既定値を指定する。|| だと 0 が既定値に置き換わってしまう。
type Product = {
name: string;
stock?: number;
};
const item: Product = { name: "サンプル品", stock: 0 };
console.log(`在庫:${item.stock ?? 0}個`);
もし || を使うと、在庫0が「未設定」と同じ扱いになる。0という正しい値を潰さないために ?? が必要になる。
出力 == ハンドラなし
handler.onClick?.() と書くと、関数が無ければ呼び出さずに undefined を返す。
type Handler = {
onClick?: () => string;
};
const handler: Handler = {};
console.log(handler.onClick?.() ?? "ハンドラなし");
関数の呼び出しにも ?. が使える。?.() と書くと「あれば呼ぶ」という意味になり、コールバックが任意の場面で役立つ。
4実践シナリオ(5問)
①田中:email のみ ②鈴木:phone のみ ③佐藤:どちらも無し コーディング★★☆無料
出力3行が完全一致
?? で「未登録」を既定値にする。
type User = {
id: number;
name: string;
email?: string;
phone?: string;
};
const users: User[] = [
{ id: 1, name: "田中", email: "a@b.com" },
{ id: 2, name: "鈴木", phone: "090-1111-2222" },
{ id: 3, name: "佐藤" },
];
for (const u of users) {
console.log(`${u.name}:${u.email ?? "未登録"} / ${u.phone ?? "未登録"}`);
}
任意項目が複数あっても、書き方は同じ。実務のユーザー情報は必須項目のほうが少ないので、この形が標準になる。
出力3行が完全一致
address が無い場合と、address はあるが zip が無い場合を区別する。
type Company = {
name: string;
address?: {
city: string;
zip?: string;
};
};
const companies: Company[] = [
{ name: "A社" },
{ name: "B社", address: { city: "東京" } },
{ name: "C社", address: { city: "大阪", zip: "530-0001" } },
];
for (const c of companies) {
if (!c.address) {
console.log(`${c.name}:住所未登録`);
continue;
}
console.log(`${c.name}:${c.address.city}(〒${c.address.zip ?? "未登録"})`);
}
if で address の有無を確認した後は、TypeScript が address を「必ずある」と理解する。だから c.address.city と ?. 無しで書ける——これが型の絞り込み(TS09で詳しく扱う)。
「|| の結果:未設定」
「?? の結果:0」 コーディング★★☆無料
出力2行が完全一致
stock?: number に 0 を入れ、それぞれの演算子で既定値「未設定」を指定してみる。
type Product = {
name: string;
stock?: number;
};
const item: Product = { name: "サンプル品", stock: 0 };
console.log(`|| の結果:${item.stock || "未設定"}`);
console.log(`?? の結果:${item.stock ?? "未設定"}`);
同じ 0 という値が、演算子によって別の意味になる。「在庫0個」と「在庫が未登録」はまったく違う状態なので、?? で区別する必要がある。
出力 == 合計:5,940円
各項目を ?? で既定値に置き換えてから計算する。4800 + 600 = 5400、税込 5940。
type Config = {
taxRate?: number;
shippingFee?: number;
freeShippingThreshold?: number;
};
const config: Config = {};
const taxRate = config.taxRate ?? 0.1;
const shippingFee = config.shippingFee ?? 600;
const threshold = config.freeShippingThreshold ?? 5000;
const price = 4800;
const shipping = price >= threshold ? 0 : shippingFee;
const total = Math.round((price + shipping) * (1 + taxRate));
console.log(`合計:${total.toLocaleString()}円`);
設定は「省略されたら既定値」が基本。すべての項目を必須にすると、使う側が毎回全部書く必要があり不便になる。?? で既定値を用意しておけば、必要な項目だけ上書きできる。
出力2行。ノートPC:128,000円 / 該当なし:0円
find の結果は undefined になりうる。?. と ?? を組み合わせて安全に取り出す。
type Product = {
id: number;
name: string;
price: number;
};
const products: Product[] = [
{ id: 1, name: "ノートPC", price: 128000 },
];
for (const id of [1, 99]) {
const found = products.find((p) => p.id === id);
const name = found?.name ?? "該当なし";
const price = found?.price ?? 0;
console.log(`${name}:${price.toLocaleString()}円`);
}
TS03の仕上げ課題を、三項演算子なしで書けた。?. と ?? の組み合わせは、「無いかもしれない値」を扱う最も簡潔な方法になる。
5仕上げ課題
型の定義
type Address = { city: string; street?: string };
type Member = {
id: number;
name: string;
email?: string;
address?: Address;
points?: number;
};
データ(3件)
① 田中:email あり、住所(東京・丸の内)、ポイント1500
② 鈴木:住所(大阪)のみ(street なし)、ポイント0
③ 佐藤:名前のみ
出力仕様:各会員につき2行、計6行
・1行目:【田中】a@b.com(email が無ければ「メール未登録」)
・2行目:東京 丸の内 / 1,500pt(street が無ければ市区のみ、住所が無ければ「住所未登録」)
期待される出力(6行)
【田中】a@b.com
東京 丸の内 / 1,500pt
【鈴木】メール未登録
大阪 / 0pt
【佐藤】メール未登録
住所未登録 / 0pt
※ 鈴木のポイントが「0pt」と正しく表示されるかが重要。
出力6行が完全一致
住所は3パターン(無し/市区のみ/市区+番地)を分ける。ポイントは ?? 0 で既定値を指定する(|| を使うと鈴木の0が消える)。street は ?. と条件分岐を組み合わせる。
type Address = {
city: string;
street?: string;
};
type Member = {
id: number;
name: string;
email?: string;
address?: Address;
points?: number;
};
const members: Member[] = [
{
id: 1,
name: "田中",
email: "a@b.com",
address: { city: "東京", street: "丸の内" },
points: 1500,
},
{
id: 2,
name: "鈴木",
address: { city: "大阪" },
points: 0,
},
{ id: 3, name: "佐藤" },
];
for (const m of members) {
console.log(`【${m.name}】${m.email ?? "メール未登録"}`);
const city = m.address?.city;
const street = m.address?.street;
const addressText = city
? street
? `${city} ${street}`
: city
: "住所未登録";
const points = m.points ?? 0;
console.log(`${addressText} / ${points.toLocaleString()}pt`);
}
この課題で最も重要なのは、鈴木のポイントが「0pt」と表示されることだ。もし m.points || 0 と書いていたら、鈴木のポイント0は「値が無い」と誤判定され——たまたま結果は同じ0になる。だが既定値を 100 にした瞬間、ポイント0の会員に100ptが付与されるバグになる。
?? と || の違いは、こうした「0や空文字が正しい値である場面」で決定的になる。在庫0個、残高0円、コメント空文字——実務のデータには「0だが有効」という値が溢れている。
住所の処理も見てほしい。m.address?.city で安全に取り出し、その結果で分岐している。address が無い、address はあるが street が無い、両方ある——3つの状態を型が正確に表現しているからこそ、抜け漏れなく処理できる。
TS07の終わりに「すべての項目が必須なので、未登録の顧客を表現できない」と書いた。その弱点は、ここで完全に解消された。現実のデータは欠けているのが当たり前で、それを型で正直に表現できることが、TypeScriptの実用性を支えている。
次章では型ガードを学ぶ。if で確認した後になぜ型が絞り込まれるのか、その仕組みを本格的に扱う。