TypeScript / LESSON 08 / TS08

TypeScript:オプショナルとnull安全

1学習の目的

2基礎解説

やりたいこと書き方意味
省略可能な項目email?: string無くてもよい
安全にアクセスuser.email?.toUpperCase()無ければ undefined
既定値を指定user.email ?? "未登録"null/undefined のとき
入れ子も安全にa?.b?.c途中で無ければ止まる
✅ 覚えるべき重要ポイント
  1. ? を付けると省略可能な項目になるemail?: string の型は実際には string | undefined で、そのままでは使えない。
  2. ?.(オプショナルチェーン)は、値が無ければそこで止まって undefined を返すuser.email?.toUpperCase() は、email が無ければエラーにならず undefined になる。
  3. ??|| は違う??null と undefined のときだけ右側を返すが、||0"" でも右側を返してしまう(JS03)。
  4. 入れ子でも安全に辿れるcompany.address?.city なら、address が無い時点で止まるのでエラーにならない。
  5. この仕組みが無ければ、毎回 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問)

TS08-D01 User 型(id: number, name: string, email は省略可能)を定義し、email を持たないユーザーを1件作って name を出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 田中

ヒント

email?: string と書く。省略してもエラーにならない。

模範解答
type User = {
  id: number;
  name: string;
  email?: string;
};

const user: User = { id: 1, name: "田中" };

console.log(user.name);
解説

? を付けた項目は書かなくてよい。TS07までは全項目が必須だったので、「まだ登録されていない情報」を表現できなかった。

TS08-D02 email?: string の実際の型はどれか。
A. string B. string | undefined C. string | null D. any
選択★☆☆無料
期待される結果

解答 == B

ヒント

省略された場合、その値は何になるか。

模範解答
B
解説

省略可能な項目は undefined との union になる。だからそのままメソッドを呼ぶとエラーになる。

TS08-D03 email が省略されたユーザーの email を、オプショナルチェーンを使って大文字にして出力せよ(undefined と表示されることを確認する)。 コーディング★☆☆無料
期待される結果

出力 == 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 を返す。

TS08-D04 値が無い場合に既定値を返すコードを完成させよ。 穴埋め★☆☆無料
コード
const email: string | undefined = undefined;

console.log(email ____ "未登録");
期待される結果

出力 == 未登録

ヒント

null と undefined のときだけ右側を返す演算子。疑問符2つ。

模範解答
??
解説

?? は null 合体演算子|| と違い、0 や空文字を「値あり」として扱ってくれる。

TS08-D05 const n = 0; console.log(n || "既定値"); の結果はどれか。
A. 0 B. "既定値" C. undefined D. エラー
選択★☆☆無料
期待される結果

解答 == B

ヒント

0 は真偽値としては偽と扱われる(JS03)。

模範解答
B
解説

これが || の罠。在庫0個や金額0円が「未設定」と誤判定される。既定値を指定するなら ?? を使う

TS08-D06 email が省略されたユーザーと、設定されたユーザーの2件について、email があれば表示し、無ければ「未登録」と表示せよ(2行出力)。 コーディング★★☆無料
期待される結果

出力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 で分岐する必要がなく、意図も明確になる。

TS08-D07 安全にプロパティへアクセスするコードを完成させよ。 穴埋め★★☆無料
コード
type Company = { name: string; address?: { city: string } };

const c: Company = { name: "A社" };

console.log(c.address____.city ?? "住所未登録");
期待される結果

出力 == 住所未登録

ヒント

「無ければ止まる」を意味する記号。疑問符とピリオド。

模範解答
?
解説

?. は途中で止まって undefined を返す。address が無い時点で処理が止まるので、その先の city にアクセスしようとしてエラーになることがない。

TS08-D08 a?.b?.c で、a.b が undefined の場合どうなるか。
A. エラーになる B. undefined が返る C. null が返る D. 空文字が返る
選択★★☆無料
期待される結果

解答 == B

ヒント

オプショナルチェーンは途中で止まる。

模範解答
B
解説

どこで止まっても結果は undefined。深い入れ子でも、1つの ?? で既定値を指定できる。

TS08-D09 在庫数が省略可能な商品型を定義し、在庫0の商品について「在庫:0個」と正しく表示せよ(?? を使い、|| との違いを確認すること)。 コーディング★★☆無料
期待される結果

出力 == 在庫: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という正しい値を潰さないために ?? が必要になる。

TS08-D10 省略可能な関数を持つ型(onClick?: () => string)を定義し、関数が無い場合に「ハンドラなし」と出力せよ。 コーディング★★☆無料
期待される結果

出力 == ハンドラなし

ヒント

handler.onClick?.() と書くと、関数が無ければ呼び出さずに undefined を返す。

模範解答
type Handler = {
  onClick?: () => string;
};

const handler: Handler = {};

console.log(handler.onClick?.() ?? "ハンドラなし");
解説

関数の呼び出しにも ?. が使える?.() と書くと「あれば呼ぶ」という意味になり、コールバックが任意の場面で役立つ。

4実践シナリオ(5問)

TS08-S01 【会員プロフィール】User 型(id, name, email?, phone?)を定義し、3件のユーザーについて「田中:a@b.com / 未登録」の形式(email / phone)で3行出力せよ。
①田中: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 ?? "未登録"}`);
}
解説

任意項目が複数あっても、書き方は同じ。実務のユーザー情報は必須項目のほうが少ないので、この形が標準になる。

TS08-S02 【入れ子の安全なアクセス】Company 型(name, address? { city, zip? })を定義し、3社について「A社:住所未登録」「B社:東京(〒未登録)」「C社:大阪(〒530-0001)」の形式で3行出力せよ。 コーディング★★☆無料
期待される結果

出力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で詳しく扱う)。

TS08-S03 【?? と || の違いを確かめる】在庫数 0 の商品について、|| を使った場合と ?? を使った場合の結果を2行出力せよ。
「|| の結果:未設定」
「?? の結果: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個」と「在庫が未登録」はまったく違う状態なので、?? で区別する必要がある。

TS08-S04 【設定の既定値】Config 型(taxRate?: number, shippingFee?: number, freeShippingThreshold?: number)を定義し、設定が省略された場合の既定値(税率0.1、送料600、無料閾値5000)を適用して、価格4800円の合計を計算せよ。設定は空オブジェクトとし、「合計:5,940円」と出力すること。 コーディング★★★無料
期待される結果

出力 == 合計: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()}円`);
解説

設定は「省略されたら既定値」が基本。すべての項目を必須にすると、使う側が毎回全部書く必要があり不便になる。?? で既定値を用意しておけば、必要な項目だけ上書きできる。

TS08-S05 【検索結果の安全な処理】商品配列から find で商品を探し、見つからない場合も安全に「該当なし:0円」と表示せよ。id=1(ノートPC/128000)と id=99 で試し、2行出力すること。 コーディング★★★無料
期待される結果

出力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仕上げ課題

TS08-FINAL 【会員情報カードの生成】任意項目を含む会員データを安全に処理し、表示用のカードを出力せよ。

型の定義
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 で確認した後になぜ型が絞り込まれるのか、その仕組みを本格的に扱う。