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

Необязательные параметры и значения по умолчанию

Как делать параметры функций необязательными, задавать значения по умолчанию и управлять гибкостью вызовов в TypeScript.

⏱ ~18 минут 🎓 Базовый уровень 📌 Функции

🎯 Зачем нужны необязательные параметры?

В реальном коде часто бывает так, что некоторые параметры функции нужны не всегда. Например, функция создания пользователя: имя и email обязательны, а номер телефона — опционален. Без необязательных параметров вам пришлось бы создавать несколько перегруженных функций или передавать null везде.

TypeScript предоставляет элегантное решение: символ ? после имени параметра делает его необязательным. А значения по умолчанию позволяют автоматически подставить значение, если аргумент не передан.

💡 Аналогия: Представьте форму регистрации. Есть обязательные поля (имя, email) и необязательные (телефон, адрес). Необязательные можно не заполнять — форма всё равно отправится. Необязательные параметры работают так же.

✨ Символ ? — необязательные параметры

Чтобы сделать параметр необязательным, добавьте символ ? после его имени:

// phone — необязательный параметр
function createUser(name: string, email: string, phone?: string) {
  return {
    name,
    email,
    phone: phone || "Не указан"
  };
}

// Вызовы — все корректные
createUser("Иван", "ivan@mail.ru");              // ✅ OK — phone не передан
createUser("Иван", "ivan@mail.ru", "79991234567"); // ✅ OK — phone передан
createUser("Иван", "ivan@mail.ru", undefined);    // ✅ OK — phone передан как undefined

Тип необязательного параметра автоматически становится тип | undefined. Если параметр имеет тип string и помечен как ?, его полный тип — string | undefined.

// Если phone: string?, то его тип = string | undefined
function createUser(name: string, email: string, phone?: string) {
  // phone имеет тип: string | undefined

  if (phone) {
    // Внутри этой ветки phone имеет тип: string
    console.log("Телефон: " + phone.toUpperCase());
  }

  // Здесь phone может быть undefined
  console.log("Телефон: " + (phone ?? "Не указан"));
}

Правила порядка параметров

В TypeScript есть строгое правило: необязательные параметры должны идти после обязательных.

// ✅ Правильно — обязательные параметры идут первыми
function createUser(name: string, email: string, phone?: string) {}

// ✅ Правильно — можно чередовать, но необязательные в конце
function createUser(name: string, email: string, phone?: string, address?: string) {}

// ❌ Ошибка — необязательный параметр перед обязательным
function createUser(name?: string, email: string) {} // Ошибка!
// Error: A required parameter cannot follow an optional parameter.

// ❌ Ошибка — тоже неверный порядок
function createUser(phone?: string, name: string, email: string) {} // Ошибка!

❌ Почему такой порядок?

Представьте, что можно опустить обязательный параметр. Как TypeScript поймёт, передали вы "Мария" для name или для email? Чтобы избежать неоднозначности, обязательные параметры всегда идут первыми.

⚙️ Значения по умолчанию (Default Parameters)

Значения по умолчанию — это ещё один способ сделать параметры необязательными. При объявлении функции вы задаёте значение, которое будет использоваться, если аргумент не передан.

// Значение по умолчанию для language
function greet(name: string, language: string = "ru"): string {
  if (language === "ru") {
    return "Привет, " + name + "!";
  }
  if (language === "en") {
    return "Hello, " + name + "!";
  }
  return "Hi, " + name + "!";
}

// Вызовы
greet("Анна");               // "Привет, Анна!" — используется значение по умолчанию
greet("Anna", "en");         // "Hello, Anna!" — передано значение
greet("Анна", "fr");         // "Hi, Анна!" — французского нет, fallback
greet("Анна", undefined);    // "Привет, Анна!" — undefined тоже вызывает значение по умолчанию

Обратите внимание: значение по умолчанию задаётся через оператор присваивания = после двоеточия с типом.

// Синтаксис: параметр: тип = значение
function logLevel(level: string = "info", message: string): void {
  console.log(`[${level.toUpperCase()}] ${message}`);
}

// Примеры использования
logLevel(undefined, "Сервер запущен");     // [INFO] Сервер запущен
logLevel("warning", "Мало памяти");        // [WARNING] Мало памяти
logLevel("error", "Критическая ошибка");   // [ERROR] Критическая ошибка

Значения по умолчанию с разными типами

// Число по умолчанию
function calculateTax(price: number, taxRate: number = 0.2): number {
  return price * taxRate;
}
calculateTax(100);      // 20 — taxRate = 0.2
calculateTax(100, 0.1); // 10 — taxRate = 0.1

// Булево значение по умолчанию
function formatText(text: string, uppercase: boolean = false): string {
  return uppercase ? text.toUpperCase() : text;
}
formatText("hello");            // "hello"
formatText("hello", true);      // "HELLO"

// Массив по умолчанию
function getItems(category: string, tags: string[] = []): string[] {
  return [category, ...tags];
}
getItems("electronics");                    // ["electronics"]
getItems("electronics", ["sale", "new"]);   // ["electronics", "sale", "new"]

// Объект по умолчанию
function createConfig(overrides: Record = {}): Record {
  const defaults = {
    theme: "dark",
    language: "ru",
    fontSize: "14px"
  };
  return { ...defaults, ...overrides };
}
createConfig();                              // { theme: "dark", language: "ru", fontSize: "14px" }
createConfig({ theme: "light" });            // { theme: "light", language: "ru", fontSize: "14px" }

✅ Преимущество значений по умолчанию: В отличие от ?, значение по умолчанию автоматически гарантирует, что переменная не будет undefined. Это упрощает дальнейший код.

⚖️ ? vs Defaults: когда что использовать

Символ ? и значения по умолчанию решают похожую задачу, но работают по-разному. Давайте сравним их.

// Вариант 1: символ ?
function greet1(name: string, greeting?: string): string {
  // greeting может быть undefined
  const g = greeting || "Привет";
  return g + ", " + name + "!";
}

// Вариант 2: значение по умолчанию
function greet2(name: string, greeting: string = "Привет"): string {
  // greeting никогда не undefined — всегда строка
  return greeting + ", " + name + "!";
}
Критерий Символ ? Значение по умолчанию
Тип переменной T | undefined T
Нужна проверка на undefined Да Нет
Когда параметр не передан undefined Значение по умолчанию
Лучше использовать когда Нет логичного дефолта Есть логичное начальное значение

Примеры выбора

// Символ ? — когда нет логичного дефолта
function sendNotification(userId: string, message: string, recipientEmail?: string) {
  // Если email не передан — отправляем в приложение, а не на почту
  if (recipientEmail) {
    sendEmail(recipientEmail, message);
  } else {
    sendInApp(userId, message);
  }
}

// Значение по умолчанию — когда есть логичное начальное значение
function paginate(items: unknown[], page: number = 1, perPage: number = 10) {
  const start = (page - 1) * perPage;
  const end = start + perPage;
  return items.slice(start, end);
}

// Символ ? — undefined является осмысленным состоянием
function findUser(id: string): User | undefined {
  // undefined означает «пользователь не найден»
  return users.find(u => u.id === id);
}

// Значение по умолчанию — конфигурация с дефолтами
function createButton(
  label: string,
  color: string = "blue",
  size: "small" | "medium" | "large" = "medium"
) {
  return { label, color, size };
}
createButton("Отправить");                      // { label: "Отправить", color: "blue", size: "medium" }
createButton("Удалить", "red", "small");        // { label: "Удалить", color: "red", size: "small" }

📐 Порядок параметров: обязательные, необязательные, defaults

При комбинировании обязательных, необязательных параметров и значений по умолчанию существует чёткий порядок:

// Правильный порядок:
// 1. Обязательные параметры — в начале
// 2. Необязательные параметры (?) — после обязательных
// 3. Значения по умолчанию (=) — тоже после обязательных

// Пример: отправка сообщения
function sendMessage(
  recipient: string,          // Обязательный
  subject: string,            // Обязательный
  body: string,               // Обязательный
  priority?: "low" | "normal" | "high",  // Необязательный (?)
  sendAt: Date = new Date()   // С значением по умолчанию (=)
) {
  console.log(`Отправляем "${subject}" для ${recipient}`);
  console.log(`Приоритет: ${priority || "normal"}`);
  console.log(`Дата отправки: ${sendAt.toLocaleString("ru-RU")}`);
}

// Вызовы
sendMessage("user@mail.ru", "Встреча", "Завтра в 15:00");
sendMessage("admin@mail.ru", "Отчёт", "Прикреплён", "high");
sendMessage("user@mail.ru", "Напоминание", "Не забудьте", "normal", new Date("2025-07-01"));

Пропуск промежуточных параметров

Если вам нужно передать значение для позднего параметра, пропуская промежуточные, используйте undefined:

function createUser(
  name: string,
  email: string,
  phone?: string,
  address?: string,
  role: string = "user"
) {
  return { name, email, phone, address, role };
}

// Пропускаем phone и address, передаём role
createUser("Иван", "ivan@mail.ru", undefined, undefined, "admin");
// Результат: { name: "Иван", email: "ivan@mail.ru", phone: undefined, address: undefined, role: "admin" }

// Или используем объект для лучшей читаемости (см. раздел об объектах как параметрах)

📦 Деструктуризация с необязательными параметрами

Очень часто в TypeScript функции принимают объект параметров. Деструктуризация позволяет удобно извлекать значения и задавать дефолты.

// Функция принимает объект с параметрами
interface Options {
  title: string;
  description?: string;
  color: string;
  size?: "small" | "medium" | "large";
}

// Деструктуризация с дефолтами
function createCard({
  title,
  description = "Без описания",
  color = "blue",
  size = "medium"
}: Options): string {
  return `Карточка "${title}" (${size}, ${color}): ${description}`;
}

// Вызовы — все работают
createCard({ title: "Скидка", color: "red" });
// 'Карточка "Скидка" (medium, red): Без описания'

createCard({ title: "Новость", description: "Важная новость!", color: "green", size: "large" });
// 'Карточка "Новость" (large, green): Важная новость!'

// Ещё пример — конфигурация приложения
interface AppConfig {
  apiUrl: string;
  timeout: number;
  retries: number;
  debug: boolean;
}

function initApp({
  apiUrl,
  timeout = 5000,
  retries = 3,
  debug = false
}: AppConfig): void {
  console.log(`API: ${apiUrl}`);
  console.log(`Timeout: ${timeout}ms`);
  console.log(`Retries: ${retries}`);
  console.log(`Debug: ${debug}`);
}

// Вызов
initApp({
  apiUrl: "https://api.example.com"
  // timeout, retries, debug будут подставлены автоматически
});

💡 Совет: Когда функция принимает более 2-3 параметров, лучше использовать объект параметров с деструктуризацией. Это делает код читаемым и позволяет не запоминать порядок аргументов.

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

Давайте рассмотрим реальные сценарии, где необязательные параметры и значения по умолчанию делают код лучше.

Пример 1: Форматирование строки

function formatString(
  text: string,
  options: {
    uppercase?: boolean;
    trim?: boolean;
    maxLength?: number;
    suffix?: string;
  } = {}
): string {
  const {
    uppercase = false,
    trim = true,
    maxLength,
    suffix = ""
  } = options;

  let result = trim ? text.trim() : text;

  if (maxLength && result.length > maxLength) {
    result = result.slice(0, maxLength) + "...";
  }

  if (uppercase) {
    result = result.toUpperCase();
  }

  return result + suffix;
}

// Использование
formatString("  Привет, мир!  ");                    // "Привет, мир!"
formatString("  Привет, мир!  ", { uppercase: true }); // "ПРИВЕТ, МИР!"
formatString("Длинное сообщение", { maxLength: 10 });  // "Длинное со..."
formatString("hello", { uppercase: true, suffix: "!" }); // "HELLO!"

Пример 2: HTTP-запрос

interface FetchOptions {
  method?: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record;
  body?: unknown;
  timeout?: number;
  retries?: number;
}

async function apiRequest(
  url: string,
  options: FetchOptions = {}
): Promise {
  const {
    method = "GET",
    headers = {},
    body,
    timeout = 10000,
    retries = 1
  } = options;

  console.log(`${method} ${url} (timeout: ${timeout}ms, retries: ${retries})`);

  // Реальная логика запроса была бы здесь
  // ...

  return { success: true };
}

// Вызовы — всё работает гибко
await apiRequest("/api/users");                              // GET, все дефолты
await apiRequest("/api/users", { method: "POST", body: { name: "Иван" } }); // POST с телом
await apiRequest("/api/users", { timeout: 30000, retries: 3 });            // Кастомные настройки

Пример 3: Уведомления

interface NotificationOptions {
  type?: "info" | "success" | "warning" | "error";
  duration?: number;
  position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
  closable?: boolean;
  onDismiss?: () => void;
}

function showNotification(
  message: string,
  options: NotificationOptions = {}
): void {
  const {
    type = "info",
    duration = 3000,
    position = "top-right",
    closable = true,
    onDismiss
  } = options;

  console.log(`[${type.toUpperCase()}] ${message}`);
  console.log(`  Position: ${position}`);
  console.log(`  Duration: ${duration}ms`);
  console.log(`  Closable: ${closable}`);

  if (onDismiss) {
    console.log("  Callback registered for dismiss");
  }
}

// Использование
showNotification("Загрузка завершена", { type: "success" });
showNotification("Внимание!", { type: "warning", duration: 5000, position: "top-left" });
showNotification("Ошибка сервера", { type: "error", closable: false, onDismiss: () => window.location.reload() });

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

Ошибка 1: Необязательный параметр перед обязательным

// ❌ Ошибка компиляции
function createUser(name: string, email?: string, age: number) {}
// Error: A required parameter cannot follow an optional parameter.

// ✅ Правильно — все обязательные в начале
function createUser(name: string, age: number, email?: string) {}

Ошибка 2: Использование необязательного параметра без проверки

// ⚠️ Потенциальная ошибка
function greet(name: string, title?: string) {
  // ❌ title.toUpperCase() — может упасть, если title undefined
  return title.toUpperCase() + " " + name;
}

// ✅ Правильно — проверяем на undefined
function greet(name: string, title?: string) {
  if (title) {
    return title + " " + name;
  }
  return name;
}

// Или ещё лучше — с значением по умолчанию
function greet(name: string, title: string = "") {
  return (title ? title + " " : "") + name;
}

Ошибка 3: Значение по умолчанию с неправильным типом

// ❌ Ошибка типа
function calculate(price: number, discount: number = "10%") {}
// Error: Type 'string' is not assignable to type 'number'.

// ✅ Правильно — тип дефолта совпадает с типом параметра
function calculate(price: number, discount: number = 10) {}

Ошибка 4: Забыли, что ? добавляет undefined

// ⚠️ Параметр с ? имеет тип string | undefined
function processData(name?: string) {
  // ❌ TypeScript не позволит использовать строковые методы без проверки
  console.log(name.toUpperCase()); // Error: 'name' is possibly 'undefined'

  // ✅ Нужно сначала проверить
  if (name) {
    console.log(name.toUpperCase());
  }

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

🎯 Когда использовать что: шпаргалка

📋
Обязательные параметры

Используйте когда: функция не может работать без этого параметра. Пример: sendEmail(to: string) — адрес обязателен.

❓
Необязательные (?)

Используйте когда: нет логичного дефолта, undefined — осмысленное состояние. Пример: findUser(id: string): User | undefined.

⚙️
Defaults (=)

Используйте когда: есть логичное начальное значение. Пример: page: number = 1 — первая страница по умолчанию.

📋 Итоги урока

  • ? — делает параметр необязательным, его тип становится T | undefined
  • = — задаёт значение по умолчанию, параметр остаётся типа T
  • Порядок: обязательные параметры всегда идут первыми, затем необязательные и defaults
  • Деструктуризация — удобный способ работать с объектами параметров и дефолтами
  • ? лучше когда undefined — осмысленное состояние
  • = лучше когда есть логичное начальное значение

✅ Ключевое правило: Необязательные параметры и значения по умолчанию делают функции гибкими. Используйте их с умом — когда логично, а не просто чтобы «не писать лишнего».

В следующем уроке мы изучим rest-параметры и тип void — как работать с переменным числом аргументов.

Проверяем понимание

6 вопросов

Параметр по умолчанию

Premium