$ sudo teach IT
МОДУЛЬ 2 · УРОК 4

Типы null и undefined в TypeScript

Разница между ними, strict null checks и безопасная работа с отсутствующими значениями

Добро пожаловать!

null и undefined — это два особых значения в JavaScript/TypeScript, которые обозначают «отсутствие значения». Несмотря на внешнюю схожесть, между ними есть важные различия, которые необходимо понимать. В этом уроке мы разберём, чем они отличаются, как работает строгая проверка на null (strict null checks) и как безопасно работать с отсутствующими значениями.

Цели урока

1
Понять разницу между null и undefined
2
Изучить strict null checks
3
Освоить безопасную работу с отсутствующими значениями
4
Научиться использовать optional chaining и nullish coalescing

Что такое null и undefined?

Оба значения обозначают «отсутствие значения», но используются в разных ситуациях и имеют разное происхождение.

undefined

undefined означает, что переменная объявлена, но ей не присвоено значение. Это значение по умолчанию в JavaScript.

// Переменная без значения
let a: string;
console.log(a);  // undefined

// Функция без return
function greet(): void {
  console.log("Привет");
}
let result = greet();
console.log(result);  // undefined

// Доступ к несуществующему свойству
let obj = { name: "Иван" };
console.log(obj.age);  // undefined

null

null — это намеренное указание отсутствия значения. Программист явно присваивает null, чтобы показать, что значение пустое.

// Явное присваивание null
let пользователь: string | null = null;

// Результат поиска без результатов
let найденный = массив.find(x => x > 100);
// Если не найдено — вернёт undefined

// Очистка данных
let данные: string | null = "старые данные";
данные = null;  // Явно очищаем

// API может вернуть null
let ответ = await fetch("/api/user");
let пользователь = await ответ.json();
// Пользователь может быть null

Ключевое различие

undefined — это автоматическое значение по умолчанию (значение не задано). null — это намеренное указание пустоты (значение пустое). undefined устанавливается движком JavaScript, null — программистом.

Когда появляется undefined

Существует несколько типичных ситуаций, когда JavaScript возвращает undefined.

// 1. Объявленная, но не инициализированная переменная
let имя: string;
console.log(имя);  // undefined

// 2. Функция без выражения return
function ничегоНеВозвращает(): void {
  let x = 5;
}
let результат = ничегоНеВозвращает();
console.log(результат);  // undefined

// 3. Доступ к несуществующему индексу массива
let arr: number[] = [1, 2, 3];
console.log(arr[10]);  // undefined

// 4. Доступ к несуществующему свойству объекта
let obj = { a: 1, b: 2 };
console.log(obj.c);  // undefined

// 5. Результат операции с undefined
console.log(undefined + 1);  // NaN
console.log(undefined === undefined);  // true

// 6. Параметр функции по умолчанию
function привет(имя: string = "Мир") {
  console.log(`Привет, ${имя}!`);
}
привет();  // "Привет, Мир!"

Когда используется null

// 1. Явное указание отсутствия значения
let текущийПользователь: Пользователь | null = null;

// 2. Результат поиска без результатов
function найтиПользователя(id: number): Пользователь | null {
  const пользователь = базаДанных.get(id);
  return пользователь ?? null;  // Явно возвращаем null
}

// 3. Очистка значений
let токен: string | null = "abc123";
токен = null;  // Явно очищаем

// 4. DOM-методы
let элемент = document.getElementById("not-found");
console.log(элемент);  // null (если элемент не найден)

// 5. JSON
let json = '{"name": "Иван", "age": null}';
let объект = JSON.parse(json);
console.log(объект.age);  // null

// 6. Результат сравнений
let значениеА = null;
let значениеБ = undefined;
console.log(значениеА == значениеБ);  // true (нестрогое сравнение)
console.log(значениеА === значениеБ);  // false (строгое сравнение)

Strict Null Checks

TypeScript имеет опцию strictNullChecks, которая включает строгую проверку на null и undefined. Это одна из самых важных опций для написания безопасного кода.

Рекомендация

Всегда включайте strictNullChecks в tsconfig.json. Это помогает обнаруживать ошибки на этапе компиляции, связанные с отсутствующими значениями.

Без strictNullChecks

// Без strictNullChecks null и undefined присваиваются любому типу
let имя: string = null;     // Ошибки нет!
let возраст: number = undefined;  // Ошибки нет!

function найтиПользователя(id: number): Пользователь {
  // Может вернуть null, но TypeScript не знает
  return базаДанных.get(id);  // Ошибки нет!
}

let пользователь = найтиПользователя(1);
console.log(пользователь.name);  // Runtime ошибка!

С strictNullChecks

// С strictNullChecks нужен явный тип
let имя: string | null = null;        // OK
let возраст: number | undefined = undefined;  // OK

function найтиПользователя(id: number): Пользователь | null {
  return базаДанных.get(id) ?? null;
}

let пользователь = найтиПользователя(1);
// Ошибка компиляции!
// console.log(пользователь.name);  // Property 'name' does not exist on type 'Пользователь | null'

// Правильно: сначала проверяем
if (пользователь) {
  console.log(пользователь.name);  // OK
}

// Или с optional chaining
console.log(пользователь?.name);  // OK

Проверка типов (Type Guarding)

TypeScript предоставляет несколько способов проверить, что значение не является null или undefined.

Способ 1: Проверка if

let значение: string | null = "привет";

if (значение !== null) {
  console.log(значение.toUpperCase());  // "ПРИВЕТ"
}

// Или с truthy-проверкой
if (значение) {
  console.log(значение.toUpperCase());  // "ПРИВЕТ"
}

Способ 2: Optional Chaining (?.)

interface Пользователь {
  имя: string;
  адрес?: {
    город?: string;
    улица?: string;
  };
}

let пользователь: Пользователь | null = null;

// Без optional chaining — ошибка компиляции
// let город = пользователь.адрес.город;

// С optional chaining — безопасно
let город = пользователь?.адрес?.город;
console.log(город);  // undefined

// С вызовом методов
let длина = пользователь?.имя?.length;
console.log(длина);  // undefined

// С вычислением индексов
let первыйСимвол = пользователь?.имя?.[0];
console.log(первыйСимвол);  // undefined

Способ 3: Nullish Coalescing (??)

// ?? возвращает правый операнд, если левый null или undefined
let имя: string | null = null;
let значение = имя ?? "Гость";
console.log(значение);  // "Гость"

// Разница с ||
let настройки = {
  порт: 0,
  хост: ""
};

// || считает 0 и "" falsy
console.log(настройки.порт || 3000);    // 3000 (неожиданно!)
console.log(настройки.хост || "localhost"); // "localhost" (неожиданно!)

// ?? считает только null/undefined
console.log(настройки.порт ?? 3000);    // 0 (ожиданно!)
console.log(настройки.хост ?? "localhost"); // "" (ожиданно!)

Способ 4: Assertion Functions

// Функция-утверждение
function являетсяСтрокой(значение: unknown): asserts значение is string {
  if (typeof значение !== "string") {
    throw new Error(`Ожидалась строка, получено: ${typeof значение}`);
  }
}

let данные: unknown = "привет";
являетсяСтрокой(данные);
// После вызова TypeScript знает, что данные — это string
console.log(данные.toUpperCase());  // OK

// Утилита для проверки null
function существует(значение: T | null | undefined): значение is T {
  return значение !== null && значение !== undefined;
}

let пользователь: Пользователь | null = null;
if (существует(пользователь)) {
  console.log(пользователь.name);  // OK, TypeScript знает что не null
}

Оператор утверждения non-null (!)

TypeScript предоставляет оператор ! для явного указания, что значение не является null или undefined.

// Оператор ! после значения
let элемент: HTMLElement | null = document.getElementById("app");

// Без ! — ошибка компиляции
// элемент.style.color = "red";

// С ! — говорим TypeScript: "я уверен, что элемент существует"
элемент!.style.color = "red";

// Опасно! Если элемент == null, будет runtime ошибка
if (элемент) {
  элемент.style.color = "red";  // Безопасно
}

// Используйте ! только когда 100% уверены
let canvas = document.getElementById("canvas") as HTMLCanvasElement;
let ctx = canvas.getContext("2d")!;  // ctx может быть null!

Осторожно!

Оператор ! не выполняет никаких проверок во время выполнения. Он только «обманывает» компилятор. Если значение на самом деле null или undefined, вы получите ошибку времени выполнения. Используйте его только когда абсолютно уверены.

Практические примеры

Пример 1: Безопасное получение данных из localStorage

function получитьНастройки(ключ: string): Настройки | null {
  try {
    const данные = localStorage.getItem(ключ);
    if (данные === null) {
      return null;
    }
    return JSON.parse(данные) as Настройки;
  } catch {
    return null;
  }
}

// Использование
const настройки = получитьНастройки("theme");
if (настройки) {
  применитьТему(настройки.тема);
} else {
  применитьТему("светлая");
}

// Или с optional chaining и ??
const тема = получитьНастройки("theme")?.тема ?? "светлая";

Пример 2: Безопасный доступ к вложенным объектам

interface Заказ {
  id: number;
  клиент?: {
    имя?: string;
    контакты?: {
      email?: string;
      телефон?: string;
    };
  };
  товары: Товар[];
}

function получитьEmailКлиента(заказ: Заказ | null): string {
  return заказ?.клиент?.контакты?.email ?? "Не указан";
}

function получитьИмяКлиента(заказ: Заказ | null): string {
  return заказ?.клиент?.имя ?? "Анонимный клиент";
}

// Использование
const заказ: Заказ = {
  id: 1,
  клиент: {
    контакты: {
      email: "user@example.com"
    }
  },
  товары: []
};

console.log(получитьEmailКлиента(заказ));    // "user@example.com"
console.log(получитьИмяКлиента(заказ));      // "Анонимный клиент"
console.log(получитьEmailКлиента(null));     // "Не указан"

Пример 3: Поиск с возвратом null

interface Пользователь {
  id: number;
  имя: string;
  email: string;
}

const пользователи: Пользователь[] = [
  { id: 1, имя: "Иван", email: "ivan@mail.ru" },
  { id: 2, имя: "Мария", email: "maria@mail.ru" },
];

function найтиПоИди(id: number): Пользователь | null {
  return пользователи.find(п => п.id === id) ?? null;
}

function найтиПоИмени(имя: string): Пользователь | null {
  return пользователи.find(п => п.имя === имя) ?? null;
}

// Использование
const найденный = найтиПоИди(1);
if (найденный) {
  console.log(`Найден: ${найденный.имя}`);
} else {
  console.log("Пользователь не найден");
}

// Цепочка поиска
const пользователь = найтиПоИди(999) ?? найтиПоИмени("Иван");
console.log(пользователь?.имя);  // "Иван"

Пример 4: Обработка ошибок API

async function получитьДанные(url: string): Promise<Данные | null> {
  try {
    const ответ = await fetch(url);
    if (!ответ.ok) {
      return null;
    }
    const данные = await ответ.json();
    return данные as Данные;
  } catch (ошибка) {
    console.error("Ошибка загрузки:", ошибка);
    return null;
  }
}

// Использование
const данные = await получитьДанные("/api/data");
if (данные) {
  обработатьДанные(данные);
} else {
  показатьОшибку("Не удалось загрузить данные");
}

// С optional chaining и default values
const название = (await получитьДанные("/api/item"))?.название ?? "Без названия";

Частые ошибки

Ошибка 1: Путают null и undefined

// Неправильно: неясно, что именно отсутствует
let данные: string = null;  // TypeScript ошибка!

// Правильно: явно указываем тип
let данные: string | null = null;
let данные2: string | undefined = undefined;

Явно указывайте тип T | null или T | undefined.

Ошибка 2: Забывают про optional chaining

// Неправильно
let город = пользователь && пользователь.адрес && пользователь.адрес.город;

// Правильно
let город = пользователь?.адрес?.город;

Используйте optional chaining для безопасного доступа к вложенным свойствам.

Ошибка 3: Частое использование ! (non-null assertion)

// Опасно!
let элемент = document.getElementById("app")!;
элемент.style.display = "block";  // Может быть null!

// Безопасно
let элемент = document.getElementById("app");
if (элемент) {
  элемент.style.display = "block";
}

Избегайте ! — лучше выполните проверку.

Ошибка 4: Используют || вместо ?? для чисел

// Проблема: 0 и "" falsy
let порт = настройки.порт || 3000;  // 0 → 3000 (неожиданно!)
let имя = ввод || "Гость";          // "" → "Гость" (неожиданно!)

// Решение: используйте ??
let порт = настройки.порт ?? 3000;  // 0 → 0 (ожиданно!)
let имя = ввод ?? "Гость";          // "" → "" (ожиданно!)

Используйте ?? когда важны реальные значения 0 и "".

Сравнительная таблица

Характеристика null undefined
Происхождение Намеренное от программиста Автоматическое от движка
typeof "object" (баг JS!) "undefined"
JSON.stringify Не включается Не включается
== undefined true true
=== undefined false true
?? default Возвращает default Возвращает default
|| default Возвращает default Возвращает default

Подведём итоги

Вы изучили:

  • Разницу между null и undefined
  • strictNullChecks в tsconfig.json
  • Optional chaining ?.
  • Nullish coalescing ??
  • Non-null assertion !
  • Assertion functions

Важно помнить:

  • null — намеренно, undefined — по умолчанию
  • Всегда включайте strictNullChecks
  • Используйте ?. для безопасного доступа
  • Используйте ?? вместо || для 0 и ""
  • Избегайте ! — лучше проверяйте

Тест по типам null и undefined

10 вопросов

Безопасное получение значения

Premium

Безопасный доступ к свойству

Premium