Avatar ·

Как объявить nullable-тип (допускающий null) в TypeScript?

Привет! Я только начал изучать TypeScript и столкнулся с проблемой. Пишу небольшое приложение для управления задачами. У меня есть интерфейс для задачи, где поле `completedAt` должно быть `Date`, если задача выполнена, или `null`, если ещё не выполнена. Также есть поле `assignee`, которое может быть строкой с именем исполнителя или отсутствовать (`null`/`undefined`). Вот как я пытался это описать: ```typescript interface Task { id: number; title: string; completedAt: Date; assignee: string; } ``` Но TypeScript ругается, когда я пытаюсь присвоить `null` полю `completedAt`: ```typescript const task: Task = { id: 1, title: "Купить продукты", completedAt: null, // Ошибка: Type 'null' is not assignable to type 'Date'. assignee: null // Ошибка: Type 'null' is not assignable to type 'string'. }; ``` Я пробовал использовать `any`, но это убивает всю пользу от TypeScript. Видел в документации что-то про `null` и `undefined`, но не понял, как правильно указать, что поле может быть либо определённого типа, либо `null`. Подскажите, как правильно объявить такие поля? Нужно ли как-то настраивать `tsconfig.json`? И есть ли разница между `null` и `undefined` в TypeScript? Заранее спасибо! <<>>

Использование union types (объединённых типов)

В TypeScript для объявления типа, который может быть null (или undefined), используется механизм union types (объединение типов). Вы просто указываете через вертикальную черту (|) все допустимые типы для данного значения. Это стандартный, типобезопасный способ.

Исправим ваш интерфейс Task:

interface Task {
  id: number;
  title: string;
  completedAt: Date | null;  // теперь может быть Date или null
  assignee: string | null;   // теперь может быть string или null
}

// Теперь это работает:
const task: Task = {
  id: 1,
  title: "Купить продукты",
  completedAt: null, // ОК
  assignee: null     // ОК
};

// И это тоже работает:
const completedTask: Task = {
  id: 2,
  title: "Сделать отчёт",
  completedAt: new Date("2024-01-15"), // ОК
  assignee: "Иван Петров"              // ОК
};

Разница между null и undefined

По умолчанию в TypeScript null и undefined — это разные типы. Если вы хотите, чтобы поле могло быть и null, и undefined (что часто бывает при работе с API), можно написать Date | null | undefined. Однако есть более элегантный способ — использовать опциональные свойства (optional properties).

Опциональные свойства (optional properties) — более идиоматичный подход

Если null означает «значения нет», то в TypeScript часто используют undefined вместе с опциональным модификатором ?. Это делает код чище и лучше отражает намерение: «это поле может отсутствовать».

interface Task {
  id: number;
  title: string;
  completedAt?: Date;   // может быть Date или undefined
  assignee?: string;    // может быть string или undefined
}

// Теперь мы можем не указывать эти поля вообще:
const task: Task = {
  id: 1,
  title: "Купить продукты"
  // completedAt и assignee отсутствуют - это undefined
};

// Или явно присвоить undefined:
const anotherTask: Task = {
  id: 2,
  title: "Позвонить клиенту",
  completedAt: undefined,
  assignee: undefined
};

Когда использовать null, а когда undefined?

  • undefined + опциональное поле (?) — используйте, когда поле может просто отсутствовать. Это стандарт для TypeScript/JavaScript.
  • null — используйте, когда нужно явно указать «значение было сброшено/очищено». Например, если задача была выполнена, а потом статус сбросили: completedAt: null.
  • Date | null — если вы работаете с API на бэкенде (например, C# или Java), которые часто присылают именно null, а не отсутствующее поле.

Настройка tsconfig.json (strictNullChecks)

Убедитесь, что в вашем tsconfig.json включена опция strictNullChecks (она входит в состав strict: true). Без неё null и undefined будут совместимы с любым типом, что сводит на нет всю защиту:

{
  "compilerOptions": {
    "strict": true, // включает strictNullChecks и другие полезные проверки
    // или явно:
    "strictNullChecks": true
  }
}

С strictNullChecks: true (рекомендуется) TypeScript будет жёстко следить, чтобы вы не использовали null/undefined там, где они не разрешены явно. Это предотвращает множество багов в рантайме.

Проверка на null перед использованием

Когда вы получаете значение nullable-типа, TypeScript заставит вас проверить его перед использованием:

function formatTaskDate(task: Task): string {
  // Ошибка! Object is possibly 'null' или 'undefined'
  // return task.completedAt.toISOString();

  // Правильно: проверяем
  if (task.completedAt) {
    return task.completedAt.toISOString();
  }
  return "Не выполнено";
}

// Или с помощью optional chaining (?.)
function getAssignee(task: Task): string {
  return task.assignee?.toUpperCase() ?? "Нет исполнителя";
}
<<>>

Альтернативный подход: отдельные типы для разных состояний

Если в вашем проекте много логики, зависящей от наличия/отсутствия значения, можно использовать discriminated unions (размеченные объединения). Это делает состояния явными и улучшает читаемость кода:

// Явно описываем состояния задачи
type PendingTask = {
  status: 'pending';
  title: string;
  // completedAt отсутствует
};

type CompletedTask = {
  status: 'completed';
  title: string;
  completedAt: Date;
  assignee: string;
};

type Task = PendingTask | CompletedTask;

// Теперь TypeScript сам подскажет, какие поля доступны:
function processTask(task: Task) {
  if (task.status === 'pending') {
    // Здесь доступны только поля PendingTask
    console.log(`Задача "${task.title}" ожидает выполнения`);
    // task.completedAt - ошибка компиляции!
  } else {
    // Здесь доступны поля CompletedTask
    console.log(`Выполнено: ${task.completedAt.toISOString()}`);
  }
}

Этот подход особенно полезен, когда у вас сложная бизнес-логика и много различных состояний (например, заказ: новый, оплачен, отправлен, доставлен). Он делает код самодокументируемым и исключает возможность случайного использования несуществующего поля.

Минус: больше кода при объявлении типов; избыточен для простых случаев с одним-двумя nullable-полями.