Перейти к содержимому
Теория · Объектные типы · урок 4 из 7

Необязательные свойства

В некоторых анкетах рядом с полем стоит пометка «по желанию»: «Возраст — по желанию». Такое поле можно оставить пустым, и анкету всё равно примут. Но тот, кто потом её читает, не может сразу взять возраст и что-то с ним посчитать: сначала он посмотрит, заполнено ли поле.

В TypeScript такое поле — необязательное свойство. Его отмечают знаком ? после имени:

TypeScript
type User = {
    name: string;
    age?: number;
};

const ann: User = { name: "Ann", age: 30 };
const bob: User = { name: "Bob" };

console.log(ann.age); // 30
console.log(bob.age); // undefined

Разберём пример по строкам:

  1. age?: number — свойство age можно не указывать, а если оно указано, в нём число.
  2. У ann возраст есть, и компилятор проверил, что это число.
  3. У bob возраста нет, и это не ошибка — в отличие от пропущенного name.
  4. ann.age печатает 30, а bob.age — undefined: так JavaScript отвечает на чтение свойства, которого в объекте нет.

Что лежит в свойстве с ?

Прочитав bob.age, программа получает undefined. Поэтому TypeScript считает, что в необязательном свойстве лежит не number, а объединение number | undefined: может прийти число, а может не прийти ничего.

С таким объединением действует обычное правило объединений: можно только то, что подходит обоим вариантам. У undefined методов нет, поэтому сразу вызвать метод числа нельзя:

TypeScript
type User = { name: string; age?: number };

function printAge(u: User) {
    console.log(u.age.toFixed(1)); // ✗
}
TEXT
error TS18048: 'u.age' is possibly 'undefined'.

Компилятор говорит: «u.age может оказаться undefined». И он прав: если бы этот JavaScript запустили для пользователя без возраста, программа упала бы на этой строке с ошибкой TypeError: Cannot read properties of undefined (reading 'toFixed').

Сложение тоже не пройдёт: на u.age + 1 tsc выдаст ту же ошибку TS18048, хотя JavaScript молча вернул бы NaN.

Здесь аналогия с анкетой перестаёт работать. Пустое поле на бумаге видно сразу, а функция не знает, какой объект ей передадут: это решится только при запуске. Поэтому проверку пишут в коде, и после неё тип меняется. Пройдите функцию по шагам и следите за типом u.age до проверки и после неё:

ВживуюТип необязательного свойствашаг 1 из 6

Знак ? добавил к типу свойства вариант undefined.

Нажимайте «Следующий шаг»: подсказка покажет тип или ошибку на выделенной строке

Сначала проверить, потом использовать

Превратить number | undefined в number можно двумя способами.

Проверка через if

TypeScript
type User = { name: string; age?: number };

function ageText(u: User): string {
    if (u.age !== undefined) {
        return u.age.toFixed(1);
    }
    return "no age";
}

console.log(ageText({ name: "Ann", age: 30 })); // 30.0
console.log(ageText({ name: "Bob" })); // no age

Разберём, что видит компилятор:

  1. В условии u.age !== undefined у u.age тип number | undefined.
  2. Внутри if вариант undefined уже отсеян, и u.age — это number, поэтому toFixed работает.
  3. После if функция возвращает запасной текст для тех, у кого возраста нет.
  4. Программа печатает 30.0 и no age.

Проверка через if подходит, когда для пустого поля нужна своя ветка: другой текст или другое действие.

Значение по умолчанию через ??

Оператор ?? берёт значение справа, когда слева null или undefined. В записи const shown = u.age ?? 0; у shown тип number: вместо undefined подставится 0, и проверять больше нечего.

Этот способ подходит, когда у пустого поля есть разумное значение по умолчанию. Но запасное значение не должно вводить в заблуждение: возраст 0 в отчёте выглядит как настоящий, и тогда лучше отдельная ветка с if.

Частая ошибка

Необязательное свойство записали через объединение:

TypeScript
type User = { name: string; age: number | undefined };

const bob: User = { name: "Bob" }; // ✗
TEXT
error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.

Запись age: number | undefined значит другое: свойство обязательно, хотя значение в нём может быть undefined. Поэтому литерал без age не проходит, и пришлось бы писать { name: "Bob", age: undefined }. Если поле можно не указывать, нужен знак ?: age?: number. Разницу между этими двумя записями любят спрашивать на собеседованиях.

Главное

  • age?: number — свойство, которое можно не указывать в объекте.
  • Читается оно как number | undefined, и без проверки нельзя ни вызвать метод числа, ни посчитать с ним: ошибка TS18048.
  • Внутри if (u.age !== undefined) у u.age тип number, а u.age ?? 0 сразу даёт number.
  • age: number | undefined — обязательное свойство, а не необязательное.