Урок 12: Опциональные и параметры по умолчанию в TypeScript

Сегодня нас ждет очень важная и очень практичная тема. Мы научимся делать наши функции более гибкими и удобными, а код более читаемым и надежным. Речь пойдет об опциональных параметрах и параметрах по умолчанию.

В JavaScript мы часто сталкиваемся с функциями, которые могут принимать разное количество аргументов. TypeScript, как строго типизированный надстройка над JS, предоставляет нам отличные инструменты для того, чтобы описывать такое поведение безопасно и предсказуемо. Давайте же разберемся, как это работает.

Как сделать параметр функции необязательным

Представьте себе реальную жизненную ситуацию. Вы создаете функцию createUser, которая должна регистрировать нового пользователя в системе. Для этого обязательно нужно знать его email и пароль. А вот такие данные, как возраст (age) или номер телефона (phone), могут быть предоставлены, а могут и нет. В «ванильном» JavaScript мы бы просто проверили, передан ли третий или четвертый аргумент и работали бы с ними. Но как нам описать эту возможность на языке TypeScript, чтобы и компилятор понимал нас и мы сами не запутались?

Для этого в TypeScript существует специальный синтаксис: символ вопросительного знака ?, который ставится после имени параметра, который мы хотим сделать необязательным. Давайте посмотрим на примере.

typescript
function createUser(email: string, password: string, age?: number, phone?: string) {
  const user = {
    email,
    password,
    age,
    phone
  };
  console.log('User created:', user);
  // Здесь обычно код для сохранения пользователя в базу данных
  return user;
}

// Вызов функции с разным количеством аргументов
const user1 = createUser('max@mail.com', 'qwerty123'); // OK
const user2 = createUser('anna@mail.com', 'securePass', 25); // OK
const user3 = createUser('peter@mail.com', 'passWord', 30, '+79161234567'); // OK

Что же здесь происходит? Мы объявили функцию createUser с четырьмя параметрами. Первые два email и password являются обязательными. Это следует уже из самого их типа: мы указали только тип string, без знака вопроса. Следующие два параметра  age? и phone? являются опциональными. Символ ? сообщает компилятору TypeScript: «Эй, этот параметр может быть передан, а может и не быть. Это нормально, не ругайся».

Если опциональный параметр не был передан, его значение внутри функции становится undefined. Это ключевой момент! В нашем примере, если мы не передадим age, то внутри функции, при попытке обратиться к переменной age, мы получим undefined. Это поведение абсолютно идентично тому, как работает JavaScript.

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

typescript
createUser('max@mail.com'); // Ошибка компиляции!
// Expected 2-4 arguments, but got 1.

Компилятор сразу же укажет нам на ошибку: «Ожидалось от 2 до 4 аргументов, но получен только 1». Он защищает нас от потенциальных runtime-ошибок, которые обязательно возникнули бы при выполнении такого кода.

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

Установка значений по умолчанию для параметров

Отлично, с необязательными параметрами разобрались. Но что, если мы хотим не просто получить undefined на месте непереданного аргумента, а установить ему какое-то разумное значение по умолчанию? Например, в нашей функции создания пользователя, если возраст не указан, мы хотим установить его в 18. Или, скажем, мы пишем функцию приветствия, которая по умолчанию говорит «Привет, гость!», если имя не было передано.

Здесь нам на помощь приходит еще одна замечательная возможность, которая пришла в JavaScript с стандарта ES6 (и, соответственно, полностью поддерживается TypeScript), параметры по умолчанию (Default Parameters).

Синтаксис очень простой и интуитивно понятный. При объявлении функции мы можем прямо в сигнатуре указать значение для параметра, используя оператор присваивания =.

Давайте модернизируем наш предыдущий пример. Допустим, мы хотим, чтобы по умолчанию возраст пользователя считался равным 18, а номер телефона был строкой 'not provided'.

typescript
function createUser(
  email: string,
  password: string,
  age: number = 18, // Параметр по умолчанию
  phone: string = 'not provided' // Еще один параметр по умолчанию
) {
  const user = {
    email,
    password,
    age,
    phone
  };
  console.log('User created:', user);
  return user;
}

// Вызываем функцию
const userDefault = createUser('default@mail.com', 'pass'); 
// Console: User created: {email: "default@mail.com", password: "pass", age: 18, phone: "not provided"}

const userWithAge = createUser('age@mail.com', 'pass', 25);
// Console: User created: {email: "age@mail.com", password: "pass", age: 25, phone: "not provided"}

const userFull = createUser('full@mail.com', 'pass', 30, '+79161234567');
// Console: User created: {email: "full@mail.com", password: "pass", age: 30, phone: "+79161234567"}

Обратите внимание, что теперь нам даже не нужен знак вопроса ? для параметров age и phone. Почему? Потому что, установив значение по умолчанию, мы автоматически сделали эти параметры необязательными для передачи. TypeScript это понимает. Если значение по умолчанию есть, компилятор не будет требовать передачи этого аргумента.

Это очень элегантное и читаемое решение. Глядя на сигнатуру функции createUser(email: string, password: string, age: number = 18, phone: string = 'not provided'), любой разработчик сразу поймет: первые два параметра обязательны, а два последних опциональны и если их не передать, будут использованы указанные значения.

Важное замечание: в качестве значения по умолчанию можно использовать не только примитивы (числа, строки, булевы значения), но и выражения и даже вызовы других функций!

typescript
function getDefaultPhone(): string {
  return 'not provided';
}

function generateId(): number {
  return Math.floor(Math.random() * 1000);
}

function createProduct(
  name: string,
  price: number,
  id: number = generateId(), // Значение по умолчанию вычисляется при вызове
  phone: string = getDefaultPhone() // Значение по умолчанию - результат вызова функции
) {
  return { id, name, price, phone };
}

const product1 = createProduct('Laptop', 999); 
// id будет сгенерирован случайно, phone = 'not provided'

Обратите внимание, что функции generateId() и getDefaultPhone() будут вызываться именно в момент вызова createProduct, а не в момент ее объявления. Это поведение называется «ленивым вычислением».

Сочетание опциональных параметров и параметров по умолчанию

Теперь давайте разберемся с одним тонким моментом. А что, если мы попытаемся одновременно использовать и знак вопроса ? и значение по умолчанию? Нужно ли это? И что будет приоритетнее?

Давайте посмотрим на примере.

typescript
// Вариант 1: Только опциональный параметр
function greet1(name?: string) {
  console.log(`Hello, ${name}!`);
}

// Вариант 2: Опциональный параметр со значением по умолчанию
function greet2(name?: string = 'Guest') {
  console.log(`Hello, ${name}!`);
}

// Вариант 3: Параметр по умолчанию (без знака ?)
function greet3(name: string = 'Guest') {
  console.log(`Hello, ${name}!`);
}
  • Вариант 1 (greet1): Параметр name является опциональным. Если его не передать, значением name будет undefined. Следовательно, при вызове greet1() в консоль выведется Hello, undefined!, что, согласитесь, не очень красиво.

  • Вариант 2 (greet2): Этот вариант синтаксически неверен. TypeScript выдаст ошибку. Нельзя использовать одновременно ? и = .... Это избыточно и противоречиво. Компилятор говорит: «Параметр не может иметь инициализатор и быть опциональным». Вы должны выбрать что-то одно.

  • Вариант 3 (greet3): Это идеальный и правильный вариант. Мы не делаем параметр опциональным через ?, а сразу задаем ему значение по умолчанию. Это автоматически делает его необязательным для передачи. При вызове greet3() значение name будет равно 'Guest' и в консоль выведется красивый результат Hello, Guest!.

Вывод: Используйте параметры по умолчанию (parameter: type = defaultValue) вместо конструкции с опциональным параметром (parameter?: type), если вам нужно подставить конкретное значение вместо undefined. Это чище, читаемее и правильнее с точки зрения синтаксиса TypeScript.

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

Очень важный момент, который часто вызывает путаницу у новичков, это порядок следования параметров в объявлении функции. Давайте сформулируем железное правило:

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

Почему это так? Логика проста: аргументы передаются в функцию в порядке их описания. Если мы поставим опциональный параметр перед обязательным, компилятор (и сам JavaScript) просто не сможет понять, какой аргумент чему соответствует.

Рассмотрим ошибочный пример:

typescript
// НЕПРАВИЛЬНО!
function badFunction(optionalParam?: string, requiredParam: number) {
  // ...
}

badFunction(123); // Ошибка! Мы пытаемся передать number в optionalParam?: string

Как мы должны вызвать такую функцию? Мы хотим передать только requiredParam, но так как он второй по счету, мы обязаны сначала передать что-то (хотя бы undefined) для первого параметра. Это приводит к ужасной путанице.

TypeScript просто не позволит вам сделать такую ошибку. Он выдаст ошибку компиляции: «A required parameter cannot follow an optional parameter.» («Обязательный параметр не может следовать после опционального»).

Поэтому всегда соблюдайте правильный порядок:

  1. Сначала все обязательные параметры (без ? и без значения по умолчанию).

  2. Затем опциональные параметры (с ?).

  3. И в самом конце параметры с значениями по умолчанию (которые, напомним, уже являются необязательными).

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

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

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

Пример 1: Функция настройки компонента

Представьте, что вы создаете функцию для настройки UI-компонента (например, кнопки). У нее много параметров, но большинство из них имеют разумные значения по умолчанию.

typescript
function createButton(
  text: string, // Обязательный: текст кнопки
  color: string = 'blue', // По умолчанию: синий цвет
  size: 'small' | 'medium' | 'large' = 'medium', // По умолчанию: средний размер
  isDisabled: boolean = false // По умолчанию: включена
): { render: () => void } {
  // Логика создания кнопки...
  console.log(`Creating button: ${text}, ${color}, ${size}, ${isDisabled}`);

  return {
    render: () => console.log(`Button "${text}" rendered!`)
  };
}

// Создаем кнопки с разным набором параметров
const btn1 = createButton('Click me'); 
// 'Creating button: Click me, blue, medium, false'

const btn2 = createButton('Delete', 'red', 'large', true);
// 'Creating button: Delete, red, large, true'

const btn3 = createButton('Submit', 'green'); 
// 'Creating button: Submit, green, medium, false'

// btn1.render(); // Выведет: Button "Click me" rendered!

Задача 1 для закрепления

Ваша задача написать функцию formatMessage для форматирования строки сообщения.

  • Первый параметр message (строка) обязательный, само сообщение.

  • Второй параметр type (строка) тип сообщения. Может быть 'info''warning' или 'error'. По умолчанию должен быть 'info'.

  • Третий параметр isImportant (булево) флаг важности. По умолчанию false.

Функция должна возвращать отформатированную строку в виде: [type] message (Important?).
Например:

  • formatMessage('File saved') должна вернуть [info] File saved

  • formatMessage('Disk full', 'warning', true) должна вернуть [warning] Disk full (Important!)

Решение:

typescript
function formatMessage(
  message: string,
  type: 'info' | 'warning' | 'error' = 'info',
  isImportant: boolean = false
): string {
  const importanceText = isImportant ? ' (Important!)' : '';
  return `[${type}] ${message}${importanceText}`;
}

console.log(formatMessage('File saved')); // [info] File saved
console.log(formatMessage('Disk full', 'warning', true)); // [warning] Disk full (Important!)
console.log(formatMessage('Error occurred', 'error')); // [error] Error occurred

Пример 2: Расчет скидки

Напишем функцию для расчета итоговой цены товара с учетом скидки. Скидка может быть процентной или фиксированной. Но чаще применяется процентная, поэтому сделаем ее параметром по умолчанию.

typescript
function calculatePrice(
  basePrice: number,
  discount: number,
  isFixedAmount: boolean = false // Флаг: false - скидка в %, true - фиксированная сумма
): number {
  if (isFixedAmount) {
    // Если скидка фиксированная, просто вычитаем ее из базовой цены
    // Но не даем уйти в минус
    return Math.max(0, basePrice - discount);
  } else {
    // Если скидка в процентах, вычисляем процент
    // Ограничиваем скидку сверху 100%
    const discountPercent = Math.min(100, discount);
    return basePrice * (1 - discountPercent / 100);
  }
}

console.log(calculatePrice(1000, 10)); // 900 (10% от 1000)
console.log(calculatePrice(1000, 20, true)); // 980 (1000 - 20)
console.log(calculatePrice(1000, 150, true)); // 0 (1000 - 150, но не меньше 0)

Задача 2 повышенной сложности

Создайте функцию createUserProfile, которая генерирует объект с информацией о пользователе.
Обязательные параметры:

  • username (string)

  • email (string)

Опциональные параметры (с значениями по умолчанию):

  • isVerified (boolean) — по умолчанию false.

  • role (string) — по умолчанию 'user'.

  • lastLogin (Date | null) — по умолчанию null. Но если параметр не передан, нужно установить значение в текущую дату и время (используйте new Date()).

Функция должна возвращать объект со всеми этими полями.

Решение:

typescript
function createUserProfile(
  username: string,
  email: string,
  isVerified: boolean = false,
  role: string = 'user',
  lastLogin: Date | null = null // Обратите внимание, значение по умолчанию null, но мы его переопределим
) {
  // Если lastLogin не передан (равен null), используем новую дату
  const loginDate = lastLogin !== null ? lastLogin : new Date();

  return {
    username,
    email,
    isVerified,
    role,
    lastLogin: loginDate
  };
}

const profile1 = createUserProfile('max90', 'max@mail.ru');
console.log(profile1);
// { username: "max90", email: "max@mail.ru", isVerified: false, role: "user", lastLogin: [текущая дата] }

const profile2 = createUserProfile('admin', 'admin@site.com', true, 'administrator', new Date('2023-10-01'));
console.log(profile2);
// { username: "admin", ... lastLogin: Date('2023-10-01') }

В этом решении есть важный нюанс. Мы не можем написать lastLogin: Date = new Date() прямо в сигнатуре функции, потому что это установило бы одну и ту же дату (момент компиляции) для всех вызовов. Нам же нужно, чтобы для каждого нового вызова генерировалась текущая дата. Поэтому мы принимаем null по умолчанию, а внутри функции проверяем: если lastLogin === null, то подставляем new Date(). Это классический прием для «ленивой» инициализации.

Заключение

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

Давайте резюмируем ключевые моменты:

  • Опциональный параметр помечается символом ? и означает, что аргумент можно не передавать. Его значение внутри функции будет undefined.

  • Параметр по умолчанию задается через оператор = и автоматически делает параметр необязательным. Если аргумент не передан, вместо undefined подставляется указанное значение.

  • Не смешивайте ? и =. Если вам нужно значение отличное от undefined, используйте параметры по умолчанию.

  • Соблюдайте порядок. Сначала обязательные параметры, потом опциональные/с значениями по умолчанию.

  • Значением по умолчанию может быть не только примитив, но и выражение или результат вызова функции.

Эти техники делают ваш код гораздо более читаемым и выразительным. Теперь, глядя на сигнатуру функции, вы и другие разработчики сразу понимают, какие данные необходимы для ее работы, а какие можно опустить.

Не забывайте, что все уроки этого курса доступны на моем сайте. Если вы хотите изучать все системно и последовательно, добро пожаловать на полный курс.

Полный курс с уроками по TypeScript для начинающих: https://max-gabov.ru/typescript-dlya-nachinaushih

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий