TypeScriptで開発していると、「Object is possibly 'undefined'」「null と undefined の違いが分からない」「結局どう判定するのが正解なのか…」と悩んだ経験はありませんか。
JavaScriptの感覚で if (value) や value === undefined と書いていたら、TypeScriptでは思わぬコンパイルエラーが出たり、レビューで指摘されたりすることも少なくありません。特に null・undefined・空文字・0 が絡む条件分岐は、少し書き方を間違えるだけでバグの温床になりがちです。
さらに、Optional Chaining(?.)や Nullish Coalescing(??)といった新しい構文が登場したことで、「便利そうだけど正しく使えている自信がない」「|| との違いがあいまい」という方も多いのではないでしょうか。TypeScriptは型安全を高めるために null と undefined を厳密に区別しますが、その理由や設計思想を理解しないまま使うと、エラーを“消すだけのコード”になってしまいます。
この記事では、「typescript null undefined 判定」で検索した方がつまずきやすいポイントを整理しながら、実務でそのまま使える安全な判定方法を分かりやすく解説します。場当たり的な対処ではなく、「なぜその書き方が正しいのか」まで理解できる内容を目指しています。
格安ドメイン取得サービス─ムームードメイン─TypeScriptでnullとundefinedを判定する前に知るべき基礎知識
JavaScriptとTypeScriptで異なる「値がない」ことの定義
JavaScriptには「値がない」ことを表現する方法が複数存在します。主なものはnull、undefined、そして空文字""です。これらは一見似ているようで、言語仕様上は明確に異なる存在として扱われます。
let a: string | null = null; // 明示的に「値が存在しない」
let b: string | undefined = undefined; // 未定義、または初期化されていない
let c: string = ""; // 空の文字列(値は存在する)
console.log(typeof a); // "object"
console.log(typeof b); // "undefined"
console.log(typeof c); // "string"
JavaScriptでは、nullは「意図的に値がないことを示すオブジェクト」として扱われ、typeof nullは歴史的な理由から"object"を返します。一方、undefinedは「変数が宣言されたが値が代入されていない」状態を表します。
TypeScriptはこの区別をさらに厳密化し、型システムに組み込んでいます。実務では以下のような使い分けが一般的です。
interface User {
name: string;
email: string;
phoneNumber: string | null; // 電話番号は任意。ない場合はnull
avatar?: string; // アバター画像は省略可能(undefined許容)
}
const user: User = {
name: "山田太郎",
email: "yamada@example.com",
phoneNumber: null, // 電話番号を登録していない
// avatarは省略(undefinedになる)
};
このコードでは、phoneNumberは明示的に「登録されていない」ことをnullで表現し、avatarはプロパティ自体が省略可能であることを?(Optional Property)で表現しています。
なぜTypeScriptはnullとundefinedを厳密に区別するのか
TypeScriptがnullとundefinedを厳密に区別する理由は、型安全性を高めるためです。JavaScriptでは両者を曖昧に扱えますが、TypeScriptでは「意図的な値の不在(null)」と「未初期化・未定義(undefined)」を型レベルで識別できます。
// Before: JavaScriptの曖昧な扱い
function getUserName(user) {
return user.name; // userがnullやundefinedの場合、実行時エラー
}
// After: TypeScriptの型安全な扱い
function getUserName(user: User | null): string {
if (user === null) {
return "ゲスト";
}
return user.name; // この時点でuserはUser型として確定
}
この区別により、以下のメリットが得られます。
- コンパイル時のエラー検出:
nullやundefinedの可能性がある変数に対して、チェックなしでプロパティアクセスするとコンパイルエラーになります - 意図の明確化: APIのレスポンスで
nullが返ってきた場合は「データが存在しない」、undefinedの場合は「そのフィールド自体が含まれていない」といった意味の違いを表現できます - 型の絞り込み(Type Narrowing): 条件分岐で
nullチェックを行うと、その後のスコープでは型が自動的に絞り込まれます
interface APIResponse {
data: User | null; // データがない場合はnull
error?: string; // エラーメッセージは任意
}
function handleResponse(response: APIResponse) {
if (response.data === null) {
console.log("データが見つかりません");
return;
}
// この時点でresponse.dataはUser型として扱える
console.log(response.data.name); // 型補完が効く
if (response.error !== undefined) {
// errorが存在する場合の処理
console.error(response.error);
}
}
strictNullChecksとは何か|ON/OFFで何が変わるのか
TypeScriptのstrictNullChecksは、nullとundefinedの扱い方を決定する最も重要なコンパイラオプションです。tsconfig.jsonで設定できます。
{
"compilerOptions": {
"strictNullChecks": true // または "strict": true に含まれる
}
}
strictNullChecks: false の場合(レガシーモード)
// strictNullChecks: false
let name: string = null; // エラーにならない
let age: number = undefined; // エラーにならない
function greet(user: User) {
console.log(user.name); // userがnullでもコンパイルエラーにならない
}
greet(null); // 実行時エラーになる可能性があるが、コンパイルは通る
strictNullChecksが無効の場合、すべての型に暗黙的にnullとundefinedが含まれます。これはJavaScriptとの互換性は高いものの、型安全性が大幅に低下します。
strictNullChecks: true の場合(推奨設定)
// strictNullChecks: true
let name: string = null; // ❌ Error: Type 'null' is not assignable to type 'string'
let age: number = undefined; // ❌ Error: Type 'undefined' is not assignable to type 'number'
// 明示的にnullを許容する必要がある
let name2: string | null = null; // ✅ OK
let age2: number | undefined = undefined; // ✅ OK
function greet(user: User | null) {
console.log(user.name); // ❌ Error: Object is possibly 'null'
if (user !== null) {
console.log(user.name); // ✅ OK(型ガードで絞り込まれている)
}
}
実務における推奨設定
モダンなTypeScriptプロジェクトでは、strictNullChecks: true(またはstrict: true)が標準です。以下の表で挙動の違いをまとめます。
| 項目 | strictNullChecks: false | strictNullChecks: true |
|---|---|---|
string型にnull代入 | ⭕ 可能 | ❌ エラー |
User型にnull代入 | ⭕ 可能 | ❌ エラー |
| null/undefinedチェック不要でプロパティアクセス | ⭕ 可能(危険) | ❌ エラー |
| 型の明示性 | 低い | 高い |
| 実行時エラーの可能性 | 高い | 低い |
| 推奨度 | 非推奨 | 強く推奨 |
// strictNullChecks: true での実践的な例
interface Product {
id: number;
name: string;
description: string | null; // 説明文は任意
stock?: number; // 在庫数は省略可能
}
function displayProduct(product: Product | null) {
// productのnullチェック
if (product === null) {
console.log("商品が見つかりません");
return;
}
// descriptionのnullチェック
const desc = product.description ?? "説明なし";
// stockのundefinedチェック
const stockInfo = product.stock !== undefined
? `在庫: ${product.stock}個`
: "在庫情報なし";
console.log(`${product.name}: ${desc} (${stockInfo})`);
}
strictNullChecksを有効にすることで、コンパイル時に潜在的なバグを発見でき、コードレビューでも「なぜnullチェックがないのか」といった指摘を受けにくくなります。新規プロジェクトでは必ず有効化し、既存プロジェクトでも段階的に移行することを強く推奨します。
コストパフォーマンスに優れた高性能なレンタルサーバー
【Hostinger】TypeScriptにおけるnull・undefinedの代表的な判定パターン

value === null と value === undefined の正しい使い分け
TypeScriptでnullとundefinedを判定する最も基本的かつ推奨される方法は、厳密等価演算子===を使った比較です。これにより、型の曖昧さを排除し、意図を明確にできます。
function processUser(user: User | null | undefined) {
// nullの判定
if (user === null) {
console.log("ユーザーは明示的に存在しません");
return;
}
// undefinedの判定
if (user === undefined) {
console.log("ユーザーは未設定です");
return;
}
// この時点でuserはUser型として確定
console.log(user.name); // 型補完が効く
}
使い分けの実践的な指針
interface APIResponse {
user: User | null; // データがない場合はnull
settings?: { // settingsプロパティ自体が省略可能
theme: string;
};
}
function handleAPIResponse(response: APIResponse) {
// nullチェック:APIが「ユーザーが存在しない」と明示的に返した
if (response.user === null) {
console.log("ユーザーが見つかりませんでした");
return;
}
// undefinedチェック:オプショナルなプロパティの存在確認
if (response.settings === undefined) {
console.log("設定が未定義です");
// デフォルト設定を使用
} else {
console.log(`テーマ: ${response.settings.theme}`);
}
console.log(`ユーザー名: ${response.user.name}`);
}
型ガードとしての効果
===による判定は、TypeScriptの型システムに正しく認識され、コントロールフロー分析(Control Flow Analysis)によって型が自動的に絞り込まれます。
function getDisplayName(name: string | null | undefined): string {
if (name === null) {
return "名前なし";
}
if (name === undefined) {
return "未設定";
}
// この時点でnameはstring型として確定
return name.toUpperCase(); // nullやundefinedの可能性がないため安全
}
「===」ではなく「==」を唯一使っても良いケースとその理由
JavaScriptの==(等価演算子)は型強制を伴うため、一般的には避けるべきとされています。しかし、nullとundefinedの両方を同時に判定する場合に限り、== nullの使用が公式に推奨されています。
// ❌ 冗長な書き方
if (value === null || value === undefined) {
console.log("値が存在しません");
}
// ✅ 推奨される簡潔な書き方
if (value == null) {
console.log("値が存在しません");
}
なぜ== nullは例外的に許容されるのか
JavaScriptの仕様では、==を使った場合、以下の挙動が保証されています。
null == undefined // true
null == null // true
undefined == undefined // true
// それ以外の値はすべてfalse
0 == null // false
"" == null // false
false == null // false
つまり、value == nullは「valueがnullまたはundefinedの場合のみtrue」となり、他のfalsy値(0、””、falseなど)を誤って判定することがありません。
実務での活用例
interface FormData {
username: string;
email: string;
phoneNumber?: string; // 省略可能
address: string | null; // nullの可能性がある
}
function validateFormData(data: FormData) {
// phoneNumberとaddressの両方をチェック
// どちらかがnullまたはundefinedの場合に警告
if (data.phoneNumber == null || data.address == null) {
console.warn("一部の情報が未入力です");
}
// より詳細な判定が必要な場合は === を使う
if (data.address === null) {
console.log("住所が明示的にnullです");
} else if (data.address === undefined) {
console.log("住所が未定義です"); // 通常はここには来ない(型定義上nullのみ)
}
}
ESLintでの扱い
多くのプロジェクトで使われるESLintのeqeqeqルールでは、== nullのみを例外として許可するオプションがあります。
// .eslintrc.json
{
"rules": {
"eqeqeq": ["error", "always", { "null": "ignore" }]
}
}
この設定により、== nullのみが許可され、その他の==の使用はエラーとなります。
判定方法の比較表
| 判定方法 | null | undefined | 0 | “” | false | NaN | 推奨度 |
|---|---|---|---|---|---|---|---|
value === null | ⭕ | ❌ | ❌ | ❌ | ❌ | ❌ | ⭐⭐⭐ |
value === undefined | ❌ | ⭕ | ❌ | ❌ | ❌ | ❌ | ⭐⭐⭐ |
value == null | ⭕ | ⭕ | ❌ | ❌ | ❌ | ❌ | ⭐⭐⭐ |
!value | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ⚠️ 注意 |
// 実践例:APIレスポンスの処理
async function fetchUserProfile(userId: string): Promise<User | null> {
const response = await api.getUser(userId);
// responseがnullまたはundefinedの場合を一括判定
if (response == null) {
console.log("ユーザーデータの取得に失敗しました");
return null;
}
return response;
}
function displayUserInfo(user: User | null | undefined) {
// nullとundefinedを区別する必要がない場合
if (user == null) {
console.log("ユーザー情報がありません");
return;
}
// この時点でuserはUser型
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
}
typeof value === “undefined” が必要になるケース
通常、value === undefinedで十分ですが、変数が宣言されているかどうかが不明な場合にはtypeofを使う必要があります。
// ケース1:グローバル変数の存在確認
// ❌ これはReferenceErrorになる可能性がある
if (window.myGlobalVar === undefined) {
// myGlobalVarが宣言されていない場合、ここでエラー
}
// ✅ 安全な確認方法
if (typeof window.myGlobalVar === "undefined") {
console.log("myGlobalVarは存在しません");
}
実務での具体例
// ブラウザ環境かどうかの判定
if (typeof window !== "undefined") {
// ブラウザ環境での処理
window.localStorage.setItem("key", "value");
} else {
// Node.js環境での処理
console.log("サーバーサイドで実行中");
}
// Next.jsでのSSR対応コード
function useLocalStorage(key: string) {
if (typeof window === "undefined") {
// サーバーサイドレンダリング時
return null;
}
// クライアントサイドのみで実行
return window.localStorage.getItem(key);
}
ケース2:オプショナルプロパティの判定
通常の変数やプロパティに対しては、=== undefinedで十分です。
interface Config {
apiUrl: string;
timeout?: number; // オプショナル
}
function createClient(config: Config) {
// ✅ これで十分(通常の推奨方法)
if (config.timeout === undefined) {
config.timeout = 5000; // デフォルト値を設定
}
// typeof は不要(プロパティは必ず存在するか undefined)
// if (typeof config.timeout === "undefined") { } // 冗長
}
ケース3:外部ライブラリやAPIの型定義が不正確な場合
// 外部ライブラリの型定義が不完全な場合
declare const externalLib: any;
function useExternalFeature() {
// ライブラリの特定の機能が存在するかチェック
if (typeof externalLib.someFeature === "undefined") {
console.log("この機能はサポートされていません");
return;
}
externalLib.someFeature();
}
判定方法の選択フローチャート
// 判定方法の選択ガイド
function checkValue(value: unknown) {
// 1. 変数が宣言されているか不明な場合(グローバル変数など)
if (typeof someGlobalVar === "undefined") { }
// 2. nullとundefinedを区別したい場合
if (value === null) { }
if (value === undefined) { }
// 3. nullとundefinedをまとめて判定したい場合
if (value == null) { }
// 4. プロパティの存在確認(通常のケース)
const obj: { prop?: string } = {};
if (obj.prop === undefined) { } // これで十分
}
まとめ:判定パターンの使い分け
// 実践的な統合例
interface Product {
id: number;
name: string;
price: number | null; // 価格未定の場合null
description?: string; // 説明文は省略可能
stock?: number | null; // 在庫情報は省略可能、または未確定
}
function displayProduct(product: Product | null | undefined) {
// パターン1: nullとundefinedをまとめて判定
if (product == null) {
console.log("商品情報がありません");
return;
}
// パターン2: nullのみを厳密に判定
if (product.price === null) {
console.log(`${product.name}: 価格は後日公開`);
} else {
console.log(`${product.name}: ¥${product.price.toLocaleString()}`);
}
// パターン3: undefinedのみを判定(オプショナルプロパティ)
if (product.description === undefined) {
console.log("説明文なし");
} else {
console.log(product.description);
}
// パターン4: nullとundefinedの両方があり得る場合
if (product.stock == null) {
console.log("在庫情報なし");
} else if (product.stock === 0) {
console.log("在庫切れ");
} else {
console.log(`在庫: ${product.stock}個`);
}
}
これらのパターンを適切に使い分けることで、型安全性を保ちながら、可読性の高いコードを書けるようになります。
空文字・0・NaNを含めるか?条件別の「falsy判定」使い分け一覧
null / undefined / 空文字を一括判定したいときの条件分岐パターン集
実務では、nullとundefinedだけでなく、空文字列も「値がない」として扱いたい場面が頻繁にあります。フォームのバリデーションやユーザー入力の検証がその典型例です。
パターン1: 文字列の空チェック(最も一般的)
function validateUsername(username: string | null | undefined): boolean {
// ❌ 誤った判定:空文字を見逃す
if (username === null || username === undefined) {
return false;
}
// usernameが""の場合はtrueを返してしまう
// ✅ 正しい判定:null / undefined / 空文字をすべて弾く
if (username == null || username === "") {
return false;
}
return username.length >= 3;
}
// より簡潔な書き方
function validateUsername2(username: string | null | undefined): boolean {
// 空文字の場合も!usernameでfalseになる
if (!username) {
return false;
}
// この時点でusernameはstring型(型ガードが効く)
return username.length >= 3;
}
パターン2: trimを併用した実践的なバリデーション
interface FormInput {
email: string;
name: string;
comment?: string;
}
function validateFormInput(input: FormInput): string[] {
const errors: string[] = [];
// emailの検証:null/undefined/空文字/空白文字のみを弾く
if (!input.email || input.email.trim() === "") {
errors.push("メールアドレスは必須です");
}
// nameの検証:同様に空白文字のみも弾く
if (!input.name?.trim()) {
errors.push("名前は必須です");
}
// commentの検証:オプショナルなので、値がある場合のみチェック
if (input.comment !== undefined && input.comment.trim() === "") {
errors.push("コメントは空白のみにできません");
}
return errors;
}
// 使用例
const result1 = validateFormInput({
email: " ", // 空白のみ
name: "", // 空文字
});
// result1: ["メールアドレスは必須です", "名前は必須です"]
const result2 = validateFormInput({
email: "user@example.com",
name: "山田太郎",
comment: " ", // 空白のみ
});
// result2: ["コメントは空白のみにできません"]
パターン3: 条件別の判定パターン一覧
// ケース1: null / undefined のみを判定したい
function checkNullish(value: string | null | undefined): boolean {
return value == null; // nullとundefinedのみtrue
}
// ケース2: null / undefined / 空文字を判定したい
function checkEmpty(value: string | null | undefined): boolean {
return !value; // nullとundefinedと空文字でtrue
// または: value == null || value === ""
}
// ケース3: null / undefined / 空文字 / 空白文字のみを判定したい
function checkBlank(value: string | null | undefined): boolean {
return !value || value.trim() === "";
// または: value == null || value.trim() === ""
}
// 使用例
console.log(checkNullish(null)); // true
console.log(checkNullish(undefined)); // true
console.log(checkNullish("")); // false
console.log(checkNullish(" ")); // false
console.log(checkEmpty(null)); // true
console.log(checkEmpty(undefined)); // true
console.log(checkEmpty("")); // true
console.log(checkEmpty(" ")); // false(空白文字は含まれている)
console.log(checkBlank(null)); // true
console.log(checkBlank(undefined)); // true
console.log(checkBlank("")); // true
console.log(checkBlank(" ")); // true(空白のみ)
console.log(checkBlank("hello")); // false
パターン4: 配列やオブジェクトの空チェック
function checkArrayEmpty<T>(arr: T[] | null | undefined): boolean {
return arr == null || arr.length === 0;
}
function checkObjectEmpty(obj: Record<string, unknown> | null | undefined): boolean {
return obj == null || Object.keys(obj).length === 0;
}
// 使用例
console.log(checkArrayEmpty(null)); // true
console.log(checkArrayEmpty(undefined)); // true
console.log(checkArrayEmpty([])); // true
console.log(checkArrayEmpty([1, 2, 3])); // false
console.log(checkObjectEmpty(null)); // true
console.log(checkObjectEmpty({})); // true
console.log(checkObjectEmpty({ key: "value" })); // false
実務での統合例:ユーザー登録フォーム
interface RegistrationForm {
username: string;
email: string;
password: string;
phoneNumber?: string;
bio?: string;
}
function validateRegistration(form: RegistrationForm): {
isValid: boolean;
errors: Record<string, string>;
} {
const errors: Record<string, string> = {};
// username: 必須、3文字以上
if (!form.username?.trim()) {
errors.username = "ユーザー名は必須です";
} else if (form.username.trim().length < 3) {
errors.username = "ユーザー名は3文字以上で入力してください";
}
// email: 必須、形式チェック
if (!form.email?.trim()) {
errors.email = "メールアドレスは必須です";
} else if (!/^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$/.test(form.email)) {
errors.email = "有効なメールアドレスを入力してください";
}
// password: 必須、8文字以上
if (!form.password) {
errors.password = "パスワードは必須です";
} else if (form.password.length < 8) {
errors.password = "パスワードは8文字以上で入力してください";
}
// phoneNumber: 任意、値がある場合のみ検証
if (form.phoneNumber !== undefined && form.phoneNumber.trim() === "") {
errors.phoneNumber = "電話番号は空白のみにできません";
}
// bio: 任意、値がある場合は文字数制限
if (form.bio !== undefined && form.bio.trim().length > 200) {
errors.bio = "自己紹介は200文字以内で入力してください";
}
return {
isValid: Object.keys(errors).length === 0,
errors,
};
}
if (value) が危険な理由|0やfalseが弾かれる落とし穴
if (value)によるシンプルな判定は便利ですが、JavaScriptのfalsy値の挙動を理解していないと、予期しないバグを引き起こします。
JavaScriptのfalsy値一覧
以下の値はすべてif文でfalseとして扱われます。
const falsyValues = [
false, // boolean
0, // number
-0, // number
0n, // bigint
"", // string
null, // null
undefined, // undefined
NaN, // number
];
falsyValues.forEach(value => {
if (value) {
console.log("truthy");
} else {
console.log("falsy"); // すべてここに来る
}
});
実務でよくある落とし穴
// ❌ 危険な例1:数値の0が弾かれる
function displayCount(count: number | null | undefined) {
if (!count) {
return "データがありません";
}
return `件数: ${count}`;
}
console.log(displayCount(0)); // "データがありません" ← 間違い!
console.log(displayCount(5)); // "件数: 5"
console.log(displayCount(null)); // "データがありません"
// ✅ 正しい実装
function displayCountCorrect(count: number | null | undefined) {
if (count == null) { // nullとundefinedのみを判定
return "データがありません";
}
return `件数: ${count}`;
}
console.log(displayCountCorrect(0)); // "件数: 0" ← 正しい!
console.log(displayCountCorrect(5)); // "件数: 5"
console.log(displayCountCorrect(null)); // "データがありません"
// ❌ 危険な例2:booleanのfalseが判定を誤らせる
interface UserSettings {
notifications: boolean | null; // null = 未設定
darkMode: boolean | null;
}
function applySettings(settings: UserSettings) {
// 間違った判定
if (!settings.notifications) {
console.log("通知設定が未設定です"); // falseの場合も「未設定」になってしまう
}
// 正しい判定
if (settings.notifications === null) {
console.log("通知設定が未設定です");
} else if (settings.notifications) {
console.log("通知が有効です");
} else {
console.log("通知が無効です"); // false の正しい処理
}
}
applySettings({ notifications: false, darkMode: null });
// 間違った判定: "通知設定が未設定です"
// 正しい判定: "通知が無効です"
falsy判定が安全なケース
文字列の空チェックなど、0やfalseが含まれないことが型で保証されている場合は、if (value)が有効です。
// ✅ 安全な使用例:文字列のみの場合
function greet(name: string | null | undefined) {
if (!name) { // nameは文字列なので、0やfalseの心配がない
return "こんにちは、ゲストさん";
}
return `こんにちは、${name}さん`;
}
// ✅ 安全な使用例:配列のみの場合
function processItems(items: string[] | null | undefined) {
if (!items || items.length === 0) {
console.log("アイテムがありません");
return;
}
items.forEach(item => console.log(item));
}
判定方法の選択表
| 判定したい対象 | 推奨される判定方法 | 避けるべき方法 |
|---|---|---|
| null / undefined のみ | value == null | !value |
| null / undefined / 空文字 | !value または value == null \|\| value === "" | – |
| 数値の null / undefined | value == null | !value(0が弾かれる) |
| boolean の null / undefined | value === null | !value(falseが弾かれる) |
| 配列の空 | arr == null \|\| arr.length === 0 | !arr |
| オブジェクトの空 | obj == null \|\| Object.keys(obj).length === 0 | !obj |
Boolean(value) や !!value を使うときにハマりやすい落とし穴と回避策
Boolean(value)や二重否定!!valueは、値を明示的にboolean型に変換する手法ですが、これもfalsy値の挙動を理解していないと危険です。
Boolean() と !! の基本
// これらは完全に同じ挙動
console.log(Boolean(0)); // false
console.log(!!0); // false
console.log(Boolean("hello")); // true
console.log(!!"hello"); // true
console.log(Boolean(null)); // false
console.log(!!null); // false
落とし穴1:数値の0や空文字が意図せずfalseになる
interface Product {
price: number | null;
stock: number | null;
}
// ❌ 危険なコード
function isProductAvailable(product: Product): boolean {
return !!product.price && !!product.stock;
}
const freeItem = { price: 0, stock: 10 }; // 無料商品
console.log(isProductAvailable(freeItem)); // false ← 間違い!
const outOfStock = { price: 1000, stock: 0 }; // 在庫切れ
console.log(isProductAvailable(outOfStock)); // false ← これは正しい
// ✅ 正しいコード
function isProductAvailableCorrect(product: Product): boolean {
return product.price != null && product.stock != null && product.stock > 0;
}
console.log(isProductAvailableCorrect(freeItem)); // true ← 正しい!
console.log(isProductAvailableCorrect(outOfStock)); // false
落とし穴2:配列のフィルタリングで意図しない除外が発生
// ❌ 危険な例:0やfalseが除外されてしまう
const mixedArray = [1, 0, 2, false, 3, "", 4, null, 5, undefined];
const filtered = mixedArray.filter(item => !!item);
console.log(filtered); // [1, 2, 3, 4, 5] ← 0とfalseと空文字も除外された
// ✅ 正しい例:nullとundefinedのみ除外
const filteredCorrect = mixedArray.filter(item => item != null);
console.log(filteredCorrect); // [1, 0, 2, false, 3, "", 4, 5] ← 0とfalseは残る
落とし穴3:型ガードとして機能しない場合がある
function processValue(value: string | number | null | undefined) {
if (!!value) {
// valueの型は string | number | false のまま
// nullとundefinedは除外されているが、0や空文字の可能性も残る
console.log(value.toString()); // 型エラーにはならないが、0の場合は"0"になる
}
}
// より明確な型ガード
function processValueCorrect(value: string | number | null | undefined) {
if (value != null) {
// valueの型は string | number に絞り込まれる
console.log(value.toString()); // 安全に使用できる
}
}
!! を安全に使えるケース
// ✅ 使用例1:明示的なboolean変換が必要な場合
interface User {
name: string;
isAdmin: boolean | undefined;
}
function getUserRole(user: User): string {
// isAdminをbooleanに変換(undefinedをfalseとして扱う)
const isAdmin = !!user.isAdmin;
return isAdmin ? "管理者" : "一般ユーザー";
}
// ✅ 使用例2:文字列の存在チェック(0やfalseが含まれない)
function hasContent(text: string | null | undefined): boolean {
return !!text; // nullやundefinedや空文字をfalseに変換
}
// ✅ 使用例3:配列の要素数チェック
function hasItems<T>(items: T[] | null | undefined): boolean {
return !!(items && items.length);
}
実践的な回避策
// パターン1:型ごとに明示的に判定する
function validateInput(input: {
count: number | null;
enabled: boolean | null;
message: string | null;
}) {
// 数値は == null で判定
if (input.count == null) {
console.log("件数が未設定です");
} else if (input.count === 0) {
console.log("件数が0です");
}
// booleanは === null で判定
if (input.enabled === null) {
console.log("有効/無効が未設定です");
} else if (input.enabled) {
console.log("有効です");
} else {
console.log("無効です");
}
// 文字列は !value で判定可能(0やfalseの心配がない)
if (!input.message) {
console.log("メッセージが空です");
}
}
// パターン2:ヘルパー関数を作成する
function isNullish(value: unknown): value is null | undefined {
return value == null;
}
function isEmpty(value: string | null | undefined): boolean {
return value == null || value === "";
}
function isValidNumber(value: number | null | undefined): value is number {
return value != null && !Number.isNaN(value);
}
// 使用例
const userInput = {
age: 0,
name: "",
email: null,
};
console.log(isNullish(userInput.age)); // false(0は有効な値)
console.log(isEmpty(userInput.name)); // true
console.log(isValidNumber(userInput.age)); // true(0は有効な数値)
まとめ:安全な判定のためのチェックリスト
// ✅ チェックリスト
// 1. 判定対象の型を確認する
// 2. 0やfalseが有効な値として含まれるか考える
// 3. null/undefinedのみ弾きたいなら == null
// 4. 空文字も弾きたいなら !value(文字列型の場合のみ安全)
// 5. 数値やbooleanには !! を使わない
// 6. 迷ったら明示的に === null や === undefined を使う
// 実践的な統合例
interface FormData {
quantity: number | null; // 数量(0も有効)
acceptTerms: boolean | null; // 規約同意(falseも有効)
comment: string | null; // コメント(空文字は無効)
}
function validateForm(data: FormData): string[] {
const errors: string[] = [];
// 数値:0も有効なので == null で判定
if (data.quantity == null) {
errors.push("数量を入力してください");
} else if (data.quantity < 0) {
errors.push("数量は0以上で入力してください");
}
// boolean:falseも有効なので === null で判定
if (data.acceptTerms === null) {
errors.push("規約への同意が必要です");
} else if (!data.acceptTerms) {
errors.push("規約に同意してください");
}
// 文字列:空文字も無効なので !value で判定可能
if (!data.comment || data.comment.trim() === "") {
errors.push("コメントを入力してください");
}
return errors;
}
これらのパターンを理解し、状況に応じて適切な判定方法を選択することで、バグのない堅牢なコードを書けるようになります。
◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆TypeScriptの新常識!Optional Chaining(?.)とNullish Coalescing(??)
Optional Chaining(?.)でundefinedアクセスを安全に回避
Optional Chaining(?.)は、ES2020で導入され、TypeScript 3.7以降で利用可能になったモダンな構文です。ネストしたオブジェクトのプロパティアクセスを安全に行えます。
従来の冗長な書き方との比較
interface User {
name: string;
address?: {
city: string;
postalCode?: string;
};
}
const user: User | null = getUser();
// ❌ Before: 冗長なnullチェック
let city: string | undefined;
if (user !== null && user.address !== undefined) {
city = user.address.city;
}
// ✅ After: Optional Chainingで簡潔に
const city2 = user?.address?.city;
基本的な使い方
// プロパティアクセス
const userName = user?.name; // userがnullやundefinedならundefinedを返す
// ネストしたプロパティ
const postalCode = user?.address?.postalCode;
// 配列の要素アクセス
const firstItem = items?.[0];
// メソッド呼び出し
const result = obj?.method?.();
// 関数呼び出し(関数自体がnullの可能性がある場合)
const callback: (() => void) | null = getCallback();
callback?.(); // callbackがnullならば何もしない
実務での活用例1:APIレスポンスの処理
interface APIResponse {
data?: {
user?: {
profile?: {
avatar?: string;
bio?: string;
};
settings?: {
notifications?: boolean;
};
};
};
}
function displayUserInfo(response: APIResponse) {
// ❌ 従来の書き方:各階層でチェックが必要
let avatar: string | undefined;
if (response.data &&
response.data.user &&
response.data.user.profile) {
avatar = response.data.user.profile.avatar;
}
// ✅ Optional Chainingで簡潔に
const avatar2 = response.data?.user?.profile?.avatar;
const bio = response.data?.user?.profile?.bio;
const notifications = response.data?.user?.settings?.notifications;
console.log(avatar2 ?? "デフォルトアバター");
console.log(bio ?? "自己紹介なし");
console.log(notifications ?? true); // デフォルトはtrue
}
実務での活用例2:イベントハンドラー
// DOMイベントの処理
function handleClick(event: MouseEvent) {
// クリックされた要素のdata属性を安全に取得
const userId = (event.target as HTMLElement)?.dataset?.userId;
if (userId) {
loadUserProfile(userId);
}
}
// フォームのバリデーション
function validateForm(formElement: HTMLFormElement | null) {
// フォーム要素が存在する場合のみバリデーションを実行
const isValid = formElement?.checkValidity?.() ?? false;
if (!isValid) {
formElement?.reportValidity?.();
}
return isValid;
}
実務での活用例3:配列操作
interface Product {
name: string;
reviews?: {
rating: number;
comment: string;
}[];
}
function getFirstReviewRating(product: Product | null): number | undefined {
// 配列の最初の要素に安全にアクセス
return product?.reviews?.[0]?.rating;
}
function getAverageRating(product: Product | null): number {
const reviews = product?.reviews;
if (!reviews || reviews.length === 0) {
return 0;
}
const sum = reviews.reduce((acc, review) => acc + review.rating, 0);
return sum / reviews.length;
}
// 使用例
const product: Product | null = {
name: "ノートPC",
reviews: [
{ rating: 5, comment: "素晴らしい" },
{ rating: 4, comment: "良い" },
],
};
console.log(getFirstReviewRating(product)); // 5
console.log(getFirstReviewRating(null)); // undefined
Optional Chainingの動作詳細
// Optional Chainingは以下のように展開される
obj?.prop
// ↓ 実際の動作
(obj === null || obj === undefined) ? undefined : obj.prop
obj?.method()
// ↓ 実際の動作
(obj === null || obj === undefined) ? undefined : obj.method()
arr?.[index]
// ↓ 実際の動作
(arr === null || arr === undefined) ? undefined : arr[index]
注意点:Optional Chainingが評価を中断するタイミング
const obj = {
a: {
b: 0, // 0はfalsyだがnullやundefinedではない
},
};
console.log(obj?.a?.b); // 0(undefinedにならない)
const obj2 = {
a: null,
};
console.log(obj2?.a?.b); // undefined(aがnullなので以降の評価が中断される)
Nullish Coalescing(??)と論理OR(||)の決定的な違い
Nullish Coalescing(??)は、ES2020で導入された演算子で、nullまたはundefinedの場合のみデフォルト値を返します。従来の論理OR(||)との違いを理解することが重要です。
|| と ?? の挙動の違い
// || はすべてのfalsyな値でデフォルト値を返す
console.log(0 || 10); // 10
console.log("" || "default"); // "default"
console.log(false || true); // true
console.log(null || "default"); // "default"
// ?? はnullとundefinedの場合のみデフォルト値を返す
console.log(0 ?? 10); // 0
console.log("" ?? "default"); // ""
console.log(false ?? true); // false
console.log(null ?? "default"); // "default"
実務でよくあるバグと解決策
interface Config {
port: number | null;
retryCount: number | null;
enableCache: boolean | null;
}
function loadConfig(config: Config) {
// ❌ || を使った場合:0やfalseが意図せずデフォルト値になる
const port = config.port || 3000;
const retryCount = config.retryCount || 3;
const enableCache = config.enableCache || false;
console.log(port); // config.portが0の場合、3000になってしまう
console.log(retryCount); // config.retryCountが0の場合、3になってしまう
console.log(enableCache); // config.enableCacheがfalseの場合も常にfalse
// ✅ ?? を使った場合:nullとundefinedの場合のみデフォルト値
const port2 = config.port ?? 3000;
const retryCount2 = config.retryCount ?? 3;
const enableCache2 = config.enableCache ?? false;
console.log(port2); // config.portが0でも0が使われる
console.log(retryCount2); // config.retryCountが0でも0が使われる
console.log(enableCache2); // config.enableCacheがfalseならfalseが使われる
}
// 使用例
loadConfig({
port: 0, // 0は有効な値
retryCount: 0, // リトライしない設定
enableCache: false, // キャッシュを無効化
});
実践的な使用例
// 例1:ユーザー設定のデフォルト値
interface UserSettings {
theme?: "light" | "dark";
fontSize?: number;
showNotifications?: boolean;
}
function applyUserSettings(settings: UserSettings) {
// ?? を使うことで、設定が明示的にundefinedの場合のみデフォルト値を適用
const theme = settings.theme ?? "light";
const fontSize = settings.fontSize ?? 16;
const showNotifications = settings.showNotifications ?? true;
console.log(`テーマ: ${theme}, フォントサイズ: ${fontSize}px, 通知: ${showNotifications}`);
}
applyUserSettings({});
// テーマ: light, フォントサイズ: 16px, 通知: true
applyUserSettings({ theme: "dark", fontSize: 0, showNotifications: false });
// テーマ: dark, フォントサイズ: 0px, 通知: false(0とfalseが正しく保持される)
// 例2:検索パラメータの処理
interface SearchParams {
query: string;
page?: number;
limit?: number;
sortBy?: string;
}
function buildSearchQuery(params: SearchParams): string {
const page = params.page ?? 1;
const limit = params.limit ?? 20;
const sortBy = params.sortBy ?? "createdAt";
return `?query=${params.query}&page=${page}&limit=${limit}&sortBy=${sortBy}`;
}
console.log(buildSearchQuery({ query: "TypeScript" }));
// ?query=TypeScript&page=1&limit=20&sortBy=createdAt
console.log(buildSearchQuery({ query: "TypeScript", page: 0, limit: 0 }));
// ?query=TypeScript&page=0&limit=0&sortBy=createdAt(0が正しく保持される)
|| と ?? の使い分け一覧表
| シナリオ | 推奨される演算子 | 理由 |
|---|---|---|
| 数値(0も有効) | ?? | 0がデフォルト値に置き換わらない |
| boolean(falseも有効) | ?? | falseがデフォルト値に置き換わらない |
| 文字列(空文字も無効としたい) | ` | |
| 文字列(空文字も有効) | ?? | 空文字が保持される |
| オブジェクト・配列 | ?? | 一貫性のため |
// 実践的な使い分け例
interface FormInput {
username: string; // 空文字は無効
age: number | null; // 0も有効な値
agreeToTerms: boolean | null; // falseも有効な値
}
function processForm(input: FormInput) {
// usernameは空文字を無効としたいので || を使用
const username = input.username || "ゲスト";
// ageは0も有効なので ?? を使用
const age = input.age ?? 18;
// agreeToTermsはfalseも有効なので ?? を使用
const agreeToTerms = input.agreeToTerms ?? false;
return { username, age, agreeToTerms };
}
console.log(processForm({ username: "", age: 0, agreeToTerms: false }));
// { username: "ゲスト", age: 0, agreeToTerms: false }
最新TypeScriptで推奨される簡潔で可読性の高い判定構文
Optional ChainingとNullish Coalescingを組み合わせることで、極めて簡潔で安全なコードが書けます。
パターン1:?. と ?? の組み合わせ
interface User {
name: string;
profile?: {
avatar?: string;
bio?: string;
};
}
// ❌ 従来の冗長な書き方
function getUserAvatar(user: User | null | undefined): string {
let avatar: string;
if (user && user.profile && user.profile.avatar) {
avatar = user.profile.avatar;
} else {
avatar = "/default-avatar.png";
}
return avatar;
}
// ✅ モダンな書き方
function getUserAvatarModern(user: User | null | undefined): string {
return user?.profile?.avatar ?? "/default-avatar.png";
}
パターン2:配列処理との組み合わせ
interface BlogPost {
title: string;
author?: {
name: string;
posts?: BlogPost[];
};
}
function getAuthorPostCount(post: BlogPost | null): number {
// ❌ 従来の書き方
if (post && post.author && post.author.posts) {
return post.author.posts.length;
}
return 0;
// ✅ モダンな書き方
return post?.author?.posts?.length ?? 0;
}
function getFirstPostTitle(post: BlogPost | null): string {
return post?.author?.posts?.[0]?.title ?? "投稿なし";
}
パターン3:関数呼び出しとの組み合わせ
interface API {
users?: {
getById?: (id: string) => Promise<User | null>;
getAll?: () => Promise<User[]>;
};
}
async function fetchUser(api: API | null, userId: string): Promise<User | null> {
// ❌ 従来の書き方
if (api && api.users && api.users.getById) {
return await api.users.getById(userId);
}
return null;
// ✅ モダンな書き方
return await api?.users?.getById?.(userId) ?? null;
}
パターン4:複雑なネスト構造の安全なアクセス
interface Company {
departments?: {
engineering?: {
teams?: {
name: string;
members?: {
name: string;
email: string;
}[];
}[];
};
};
}
function getFirstEngineerEmail(company: Company | null): string | undefined {
// ❌ 従来の書き方(15行以上)
if (!company) return undefined;
if (!company.departments) return undefined;
if (!company.departments.engineering) return undefined;
if (!company.departments.engineering.teams) return undefined;
if (company.departments.engineering.teams.length === 0) return undefined;
const firstTeam = company.departments.engineering.teams[0];
if (!firstTeam.members) return undefined;
if (firstTeam.members.length === 0) return undefined;
return firstTeam.members[0].email;
// ✅ モダンな書き方(1行)
return company?.departments?.engineering?.teams?.[0]?.members?.[0]?.email;
}
実務での総合例:Next.jsでのデータフェッチング
interface PageProps {
data?: {
post?: {
title: string;
content: string;
author?: {
name: string;
avatar?: string;
};
tags?: string[];
relatedPosts?: {
id: string;
title: string;
}[];
};
};
}
export default function BlogPostPage({ data }: PageProps) {
// すべてのプロパティに安全にアクセス
const title = data?.post?.title ?? "タイトルなし";
const content = data?.post?.content ?? "";
const authorName = data?.post?.author?.name ?? "匿名";
const authorAvatar = data?.post?.author?.avatar ?? "/default-avatar.png";
const tags = data?.post?.tags ?? [];
const relatedCount = data?.post?.relatedPosts?.length ?? 0;
const firstRelated = data?.post?.relatedPosts?.[0]?.title;
return (
<article>
<h1>{title}</h1>
<div className="author">
<img src={authorAvatar} alt={authorName} />
<span>{authorName}</span>
</div>
<div dangerouslySetInnerHTML={{ __html: content }} />
<div className="tags">
{tags.map(tag => <span key={tag}>#{tag}</span>)}
</div>
{relatedCount > 0 && (
<aside>
<h2>関連記事({relatedCount}件)</h2>
{firstRelated && <p>最新: {firstRelated}</p>}
</aside>
)}
</article>
);
}
モダンな構文を使う際のベストプラクティス
// ✅ Good: 簡潔で可読性が高い
const email = user?.contact?.email ?? "メールアドレス未登録";
// ✅ Good: チェーンが長すぎる場合は中間変数を使う
const contact = user?.contact;
const email2 = contact?.email ?? contact?.phone ?? "連絡先未登録";
// ⚠️ 注意: チェーンが極端に長い場合は可読性が下がる
const value = obj?.a?.b?.c?.d?.e?.f?.g?.h?.i?.j; // 長すぎる
// ✅ Better: 適度に分割する
const intermediate = obj?.a?.b?.c?.d;
const value2 = intermediate?.e?.f?.g?.h?.i?.j;
// ✅ Good: デフォルト値が複雑な場合は関数化
const getUserDisplayName = (user: User | null) => {
return user?.profile?.displayName
?? user?.profile?.firstName
?? user?.email?.split("@")[0]
?? "ゲストユーザー";
};
型安全性を保ちながら簡潔に書く
// Optional ChainingとNullish Coalescingは型推論と相性が良い
interface Response {
data?: {
items?: string[];
};
}
function processResponse(response: Response) {
// itemsの型は string[] | undefined と推論される
const items = response.data?.items;
if (items) {
// この時点でitemsは string[] 型
items.forEach(item => console.log(item.toUpperCase()));
}
// デフォルト値を設定すると型が確定する
const safeItems = response.data?.items ?? []; // 型は string[]
safeItems.forEach(item => console.log(item)); // 型エラーなし
}
これらのモダンな構文を活用することで、コードの行数を削減しながら、型安全性と可読性を両立できます。TypeScript 3.7以降を使用している場合は、積極的に活用することを推奨します。
国内シェアNo.1のエックスサーバーが提供するVPSサーバー『XServer VPS』よくある質問
-
Non-null assertion operator (!) はいつ使うべきか?
-
Non-null assertion operator(
!)は、TypeScriptコンパイラに対して「この値は絶対にnullやundefinedではない」と明示的に伝える演算子です。しかし、使用は極力避けるべきです。基本的な使い方
function getElement(id: string): HTMLElement | null { return document.getElementById(id); } // ❌ Non-null assertionを使った例 const element = getElement("myButton")!; element.addEventListener("click", handleClick); // elementがnullならランタイムエラー // ✅ 推奨される書き方 const element2 = getElement("myButton"); if (element2) { element2.addEventListener("click", handleClick); } // ✅ Optional Chainingを使った書き方 getElement("myButton")?.addEventListener("click", handleClick);! を使うことの問題点
interface User { name: string; email: string; } let currentUser: User | null = null; // ❌ 危険:実行時エラーになる function displayUserName() { console.log(currentUser!.name); // currentUserがnullの場合エラー } // ✅ 安全:nullチェックを行う function displayUserNameSafe() { if (currentUser === null) { console.log("ユーザーが未設定です"); return; } console.log(currentUser.name); }例外的に ! を使っても良いケース
すぐ上の行でnullチェックをしているが、TypeScriptの型推論が効かない場合
const items: string[] | null = getItems(); if (items === null) { return; } // この時点でitemsはnullではないが、別の関数内では型が絞り込まれない setTimeout(() => { // ❌ TypeScriptはitemsがnullの可能性があると判断する // console.log(items.length); // Error // ✅ この場合のみ ! が許容される(ただし非推奨) console.log(items!.length); // ✅ より良い方法:変数を再代入 const safeItems = items; setTimeout(() => { console.log(safeItems.length); // 型推論が効く }, 100); }, 100);外部ライブラリの型定義が不正確で、確実に値が存在することが保証されている場合
// 例:ReactのuseRefで、必ず値が設定されることが分かっている場合 import { useRef, useEffect } from "react"; function Component() { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { // ❌ あまり良くない inputRef.current!.focus(); // ✅ より安全 if (inputRef.current) { inputRef.current.focus(); } // ✅ さらに良い:Optional Chaining inputRef.current?.focus(); }, []); return <input ref={inputRef} />; }テストコードなど、エラーが起きても問題ない文脈
// テストコード内では許容されることがある describe("User Service", () => { it("should return user by id", () => { const user = userService.findById("123")!; // テスト内では明示的にエラーにしたい expect(user.name).toBe("山田太郎"); }); });! の代替手段
// パターン1:型ガードを使う
function assertNonNull<T>(value: T | null | undefined, message?: string): asserts value is T {
if (value == null) {
throw new Error(message || "Unexpected null or undefined");
}
}
const element = getElement("myButton");
assertNonNull(element, "Button element not found");
element.addEventListener("click", handleClick); // 型推論が効く
// パターン2:デフォルト値を設定する
const element2 = getElement("myButton") ?? document.createElement("button");
element2.addEventListener("click", handleClick); // 常に安全
// パターン3:early returnを使う
function handleButtonClick() {
const element = getElement("myButton");
if (!element) {
console.error("Button not found");
return;
}
// この時点でelementはHTMLElement型
element.addEventListener("click", handleClick);
}
-
型アサーション(as)とNon-null assertion(!)の違いは?
-
型アサーション(
as)とNon-null assertion(!)は、どちらもTypeScriptコンパイラの型チェックを「上書き」する機能ですが、用途と危険性が異なります。型アサーション(as)の基本
// 型アサーションは「この値をこの型として扱ってください」と指示する const input = document.getElementById("username") as HTMLInputElement; input.value = "test"; // HTMLInputElementとして扱われる // 同じ意味(古い構文) const input2 = <HTMLInputElement>document.getElementById("username");Non-null assertion(!)の基本
// Non-null assertionは「この値はnullやundefinedではない」と指示する const input = document.getElementById("username")!; input.addEventListener("click", handleClick); // null チェックなしで使える違いの比較表
項目 型アサーション(as) Non-null assertion(!) 用途 型を別の型に変換 nullやundefinedを除外 構文 value as Typevalue!実行時の影響 なし なし 危険性 高い(間違った型を指定できる) 高い(実際にnullの可能性がある) 推奨度 限定的に使用 極力避ける 実務での使い分け
// ケース1:DOM要素の型アサーション // ✅ 適切な使用例:特定の型であることが確実な場合 const form = document.querySelector("form") as HTMLFormElement; const input = form.querySelector('input[name="email"]') as HTMLInputElement; // ❌ 危険な例:型が一致しない可能性がある const button = document.querySelector(".btn") as HTMLButtonElement; // .btnがdivやspanの場合、実行時エラーになる // ✅ より安全:型ガードを使う const element = document.querySelector(".btn"); if (element instanceof HTMLButtonElement) { element.disabled = true; } // ケース2:APIレスポンスの型アサーション interface User { id: string; name: string; } async function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); const data = await response.json(); // ❌ 危険:dataの構造が保証されていない return data as User; // ✅ より安全:バリデーションを行う if (typeof data.id === "string" && typeof data.name === "string") { return data as User; } throw new Error("Invalid user data"); } // ✅ さらに安全:zodなどのバリデーションライブラリを使う import { z } from "zod"; const UserSchema = z.object({ id: z.string(), name: z.string(), }); async function fetchUserSafe(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); const data = await response.json(); return UserSchema.parse(data); // バリデーションエラー時は例外を投げる }asとの組み合わせ
// as と ! を組み合わせることもできる(非推奨) const input = document.querySelector(".input") as HTMLInputElement | null; const value = input!.value; // inputがnullの可能性を無視 // ✅ より安全な書き方 const input2 = document.querySelector(".input"); if (input2 instanceof HTMLInputElement) { const value = input2.value; }安全な代替手段
// パターン1:ユーザー定義型ガード
function isHTMLInputElement(element: Element | null): element is HTMLInputElement {
return element instanceof HTMLInputElement;
}
const element = document.querySelector(".input");
if (isHTMLInputElement(element)) {
console.log(element.value); // 型安全
}
// パターン2:型述語を使った汎用ヘルパー
function assertType<T>(value: unknown, validator: (v: unknown) => v is T): T {
if (!validator(value)) {
throw new Error("Type assertion failed");
}
return value;
}
const input3 = assertType(
document.querySelector(".input"),
isHTMLInputElement
);
-
undefined と null、どちらを使うべきか?
-
実務では、
undefinedとnullのどちらを使うべきか迷うことがあります。以下のガイドラインを参考にしてください。基本的な使い分けの指針
// ✅ undefinedを使う場合:オプショナルなプロパティ interface User { name: string; email: string; phoneNumber?: string; // 省略可能な場合はundefined avatar?: string; } // ✅ nullを使う場合:明示的に「値がない」ことを表現したい interface APIResponse { user: User | null; // ユーザーが見つからない場合はnull error: string | null; // エラーがない場合はnull }それぞれの特性と使い分け
項目 undefined null 意味 未定義、初期化されていない 明示的に「値がない」 typeof "undefined""object"JSONでの扱い プロパティが省略される nullとして保持される関数の戻り値 デフォルトで undefined明示的に返す必要がある 推奨される用途 オプショナルプロパティ、デフォルト引数 APIレスポンス、明示的な「なし」 実務での使い分け例
// ケース1:関数の戻り値 // ✅ undefinedを返す:「見つからなかった」 function findUser(id: string): User | undefined { const user = users.find(u => u.id === id); return user; // 見つからない場合は自然にundefinedになる } // ✅ nullを返す:「存在しない」ことを明示 function getUserById(id: string): User | null { const user = database.query(`SELECT * FROM users WHERE id = ?`, [id]); return user || null; // 明示的にnullを返す } // ケース2:状態管理 interface AppState { currentUser: User | null; // ログインしていない場合はnull selectedItem?: Item; // 何も選択していない場合はundefined error: Error | null; // エラーがない場合はnull loadingMessage?: string; // ローディング中でない場合はundefined } // ケース3:APIレスポンス // ✅ JSONでの扱いを考慮してnullを使う interface ProductResponse { product: { id: string; name: string; description: string | null; // 説明がない場合はnull discount: number | null; // 割引なしの場合はnull } | null; // 商品が見つからない場合はnull } // JSONシリアライズ時の違い const data1 = { name: "太郎", age: undefined }; console.log(JSON.stringify(data1)); // {"name":"太郎"} (ageは省略される) const data2 = { name: "太郎", age: null }; console.log(JSON.stringify(data2)); // {"name":"太郎","age":null} (nullは保持される)チーム開発でのベストプラクティス
// プロジェクト全体で統一されたルールを決める例 // ルール1:関数の戻り値は undefined を優先 function getConfig(key: string): string | undefined { return config[key]; } // ルール2:APIレスポンスは null を優先 interface ApiUser { id: string; name: string; deletedAt: Date | null; // 削除されていない場合はnull } // ルール3:Reactのステートは null を優先 function UserProfile() { const [user, setUser] = useState<User | null>(null); const [error, setError] = useState<Error | null>(null); // ... } // ルール4:オプショナルプロパティは ? (undefined) を使う interface FormData { email: string; name: string; phoneNumber?: string; // 任意項目 bio?: string; }nullとundefinedを統一して扱う
// どちらを使っても良い場合は、統一して扱う
function isEmpty(value: string | null | undefined): boolean {
return value == null || value === "";
}
// デフォルト値を設定する場合は ?? を使う
function getDisplayName(name: string | null | undefined): string {
return name ?? "匿名ユーザー";
}
// 型定義で両方を許容する
type Nullable<T> = T | null | undefined;
function processValue(value: Nullable<string>) {
if (value == null) {
return "値なし";
}
return value.toUpperCase();
}
-
strictNullChecks を既存プロジェクトで有効化する方法は?
-
既存の大規模プロジェクトで
strictNullChecksを有効化するのは困難ですが、段階的に移行する方法があります。段階的な移行戦略
// ステップ1:tsconfig.json で段階的な有効化を設定 { "compilerOptions": { "strict": false, "strictNullChecks": false, // まずは新しいファイルのみ厳格にチェック }, "include": ["src/**/*"], "exclude": ["src/legacy/**/*"] // レガシーコードは除外 } // ステップ2:新規ファイルから厳格にチェック // 新しいファイルの先頭に以下を追加 // @ts-check // または各ファイルで /// <reference types="typescript" />ファイル単位での移行
// 移行前のコード(strictNullChecks: false) function getUserName(user: User) { return user.name; // userがnullでもエラーにならない } // 移行後のコード(strictNullChecks: true) function getUserName(user: User | null): string { if (user === null) { return "ゲスト"; } return user.name; } // または function getUserNameSafe(user: User | null): string { return user?.name ?? "ゲスト"; }実践的な移行手順
// 1. ユーティリティ関数を作成 function assertNonNull<T>(value: T | null | undefined, name: string): T { if (value == null) { throw new Error(`${name} is null or undefined`); } return value; } function orDefault<T>(value: T | null | undefined, defaultValue: T): T { return value ?? defaultValue; } // 2. 既存コードを徐々に書き換え // Before const user = getUser(); console.log(user.name); // After(段階1:assertionを使う) const user = assertNonNull(getUser(), "user"); console.log(user.name); // After(段階2:型定義を修正) const user = getUser(); // User | null を返すように修正 if (user) { console.log(user.name); } // 3. 型定義ファイルを整備 // types/legacy.d.ts declare module "legacy-module" { export function getLegacyData(): any; // 一旦anyで逃げる } // 徐々に厳密な型に置き換えていく declare module "legacy-module" { export interface LegacyData { id: string; name: string | null; } export function getLegacyData(): LegacyData | null; }移行時の注意点とESLintルール
// .eslintrc.json
{
"rules": {
// 移行期間中は警告レベルに下げる
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/strict-boolean-expressions": "off",
// 徐々に厳格にしていく
"@typescript-eslint/no-unnecessary-condition": "warn"
}
}
-
Map や Set でnull/undefinedを扱う際の注意点は?
-
MapやSetでnull/undefinedを扱う際は、通常のオブジェクトとは異なる挙動に注意が必要です。// Mapの基本的な挙動 const map = new Map<string, string | null>(); map.set("key1", "value1"); map.set("key2", null); map.set("key3", undefined); // undefinedも設定できる console.log(map.has("key1")); // true console.log(map.has("key2")); // true(nullも含まれる) console.log(map.has("key3")); // true(undefinedも含まれる) console.log(map.has("key4")); // false(存在しないキー) console.log(map.get("key1")); // "value1" console.log(map.get("key2")); // null console.log(map.get("key3")); // undefined console.log(map.get("key4")); // undefined(存在しないキー) // ⚠️ 問題:key3とkey4の区別ができない安全な判定方法
// ✅ hasメソッドで存在確認してからgetする function getMapValue<K, V>(map: Map<K, V>, key: K): V | undefined { if (map.has(key)) { return map.get(key); } return undefined; } // ✅ nullとundefinedを区別する function getMapValueOrNull<K, V>(map: Map<K, V | null>, key: K): V | null { if (!map.has(key)) { return null; // キーが存在しない } return map.get(key)!; // ここではnon-null assertionが安全 } // 実用例 const userCache = new Map<string, User | null>(); function getCachedUser(id: string): User | null | undefined { if (!userCache.has(id)) { return undefined; // キャッシュなし } return userCache.get(id)!; // null(ユーザー不在)またはUser } // 使用例 const user = getCachedUser("123"); if (user === undefined) { console.log("キャッシュにありません。APIから取得します"); } else if (user === null) { console.log("ユーザーは存在しません"); } else { console.log(`ユーザー名: ${user.name}`); }Setでのnull/undefined
// Setはnull/undefinedを値として持てる
const set = new Set<string | null | undefined>();
set.add("value");
set.add(null);
set.add(undefined);
console.log(set.has("value")); // true
console.log(set.has(null)); // true
console.log(set.has(undefined)); // true
console.log(set.size); // 3
// 実用例:入力値のユニークチェック
function getUniqueValues(items: (string | null)[]): Set<string | null> {
return new Set(items);
}
const items = ["a", "b", "a", null, "c", null];
const unique = getUniqueValues(items);
console.log(unique); // Set { "a", "b", null, "c" }
まとめ
TypeScriptにおけるnullとundefinedの判定は、一見シンプルに見えて実は奥が深く、適切な判定方法を選択することがバグのない堅牢なコードを書く上で極めて重要です。この記事で解説した内容を振り返り、実務で即座に活用できるポイントをまとめます。
基本原則:判定方法の選択フローチャート
実務では、以下のフローチャートに従って判定方法を選択することで、迷いなく適切なコードが書けます。
// 1. nullとundefinedを区別する必要があるか?
// → YES: === null または === undefined を使う
// → NO: 次へ
// 2. 空文字列も「値がない」として扱いたいか?
// → YES: !value を使う(ただし数値・booleanでないことを確認)
// → NO: value == null を使う
// 3. デフォルト値を設定したいか?
// → YES: ?? を使う(0やfalseも有効な値として扱う)
// → または || を使う(空文字をデフォルト値に置き換えたい場合)
// 4. ネストしたプロパティにアクセスしたいか?
// → YES: ?. を使う
実務で頻出するパターン別ベストプラクティス
パターン1:APIレスポンスの処理
interface APIResponse {
data: User | null;
error?: string;
}
function handleResponse(response: APIResponse) {
// nullチェックには == null が簡潔
if (response.data == null) {
console.error(response.error ?? "不明なエラー");
return;
}
// この時点でresponse.dataはUser型
displayUser(response.data);
}
パターン2:フォームバリデーション
function validateEmail(email: string | null | undefined): boolean {
// 空文字も無効としたい場合は !email
if (!email || email.trim() === "") {
return false;
}
return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
}
パターン3:設定値のデフォルト処理
interface Config {
port?: number;
timeout?: number;
retryCount?: number;
}
function loadConfig(config: Config) {
// 0も有効な値なので ?? を使う
const port = config.port ?? 3000;
const timeout = config.timeout ?? 5000;
const retryCount = config.retryCount ?? 3;
return { port, timeout, retryCount };
}
パターン4:ネストしたオブジェクトへの安全なアクセス
interface Response {
user?: {
profile?: {
avatar?: string;
};
};
}
function getAvatar(response: Response): string {
// ?. と ?? を組み合わせる
return response.user?.profile?.avatar ?? "/default-avatar.png";
}
避けるべきアンチパターン
実務でよく見かける問題のあるコードと、その改善方法をまとめます。
// ❌ アンチパターン1: 数値の判定で !value を使う
function displayCount(count: number | null) {
if (!count) { // 0がfalseになる
return "データなし";
}
return `${count}件`;
}
// ✅ 改善版
function displayCount(count: number | null) {
if (count == null) {
return "データなし";
}
return `${count}件`;
}
// ❌ アンチパターン2: Non-null assertionの乱用
const element = document.getElementById("button")!;
element.addEventListener("click", handleClick);
// ✅ 改善版
const element = document.getElementById("button");
element?.addEventListener("click", handleClick);
// ❌ アンチパターン3: デフォルト値の設定で || を使う
const config = {
retryCount: userConfig.retryCount || 3, // 0が3になってしまう
};
// ✅ 改善版
const config = {
retryCount: userConfig.retryCount ?? 3,
};
// ❌ アンチパターン4: 冗長なnullチェック
if (user !== null && user !== undefined) {
console.log(user.name);
}
// ✅ 改善版
if (user != null) {
console.log(user.name);
}
// ❌ アンチパターン5: typeof を不必要に使う
if (typeof value === "undefined") {
// 通常の変数やプロパティには不要
}
// ✅ 改善版
if (value === undefined) {
// これで十分
}
判定方法早見表
実務で迷った際にすぐ参照できる早見表です。
| 目的 | 推奨される方法 | 例 |
|---|---|---|
| nullのみを判定 | value === null | if (user === null) |
| undefinedのみを判定 | value === undefined | if (settings === undefined) |
| null/undefinedを一括判定 | value == null | if (data == null) |
| null/undefined/空文字を判定 | !value | if (!input) |
| デフォルト値設定(0/falseも有効) | value ?? defaultValue | port ?? 3000 |
| デフォルト値設定(空文字を置換) | `value | |
| ネストしたプロパティアクセス | obj?.prop?.nested | user?.profile?.avatar |
| nullチェック後のデフォルト値 | obj?.prop ?? defaultValue | user?.name ?? "匿名" |
TypeScriptのバージョン別機能対応表
使用している機能がTypeScriptのどのバージョンから利用可能かを確認しましょう。
| 機能 | TypeScriptバージョン | 備考 |
|---|---|---|
strictNullChecks | 2.0+ | 必須設定 |
Optional Chaining (?.) | 3.7+ | 推奨 |
Nullish Coalescing (??) | 3.7+ | 推奨 |
== null | すべて | 安全に使える |
! (Non-null assertion) | 2.0+ | 極力避ける |
チーム開発で統一すべきコーディング規約
プロジェクト全体で一貫性を保つため、以下の規約を定めることを推奨します。
// 1. strictNullChecksは必ず有効化
// tsconfig.json
{
"compilerOptions": {
"strict": true, // または "strictNullChecks": true
}
}
// 2. 型定義での使い分けルール
interface Example {
// オプショナルプロパティは ? を使う
optional?: string;
// 明示的に「値がない」場合は null
nullable: string | null;
// 両方あり得る場合は両方書く
maybeValue: string | null | undefined;
}
// 3. 判定方法の統一
// null/undefinedの一括判定には == null を使う
if (value == null) { }
// 個別判定には === を使う
if (value === null) { }
if (value === undefined) { }
// 4. モダンな構文を優先
// Optional Chainingを積極的に使う
const avatar = user?.profile?.avatar ?? "/default.png";
// 5. Non-null assertionは原則禁止
// ESLintで警告またはエラーにする
// "@typescript-eslint/no-non-null-assertion": "error"
TypeScriptのnull/undefined判定は、単なる構文の違いではなく、型安全性を確保し、バグを未然に防ぐための重要な設計判断です。
// この記事で学んだ内容を活かした実践例
interface UserProfile {
name: string;
email: string;
bio?: string;
avatar: string | null;
settings?: {
theme: "light" | "dark";
notifications: boolean;
};
}
function displayUserProfile(profile: UserProfile | null | undefined) {
// 1. null/undefinedチェック
if (profile == null) {
return <EmptyState />;
}
// 2. Optional Chainingでネストしたプロパティに安全にアクセス
const theme = profile.settings?.theme ?? "light";
const notifications = profile.settings?.notifications ?? true;
// 3. null/undefinedの判定を使い分け
const bio = profile.bio ?? "自己紹介が未設定です";
const avatar = profile.avatar ?? "/default-avatar.png";
return (
<div>
<img src={avatar} alt={profile.name} />
<h1>{profile.name}</h1>
<p>{profile.email}</p>
<p>{bio}</p>
<div>
<span>テーマ: {theme}</span>
<span>通知: {notifications ? "ON" : "OFF"}</span>
</div>
</div>
);
}
これらを意識してコードを書くことで、実行時エラーを大幅に減らし、可読性と保守性の高いTypeScriptコードを実現できます。あなたのプロジェクトで、今日からこれらのベストプラクティスを実践してみてください。
あなたのサイトのURL、そろそろスリムにしませんか?


