Бесплатный курс по TypeScript. Зарегистрируйтесь для отслеживания прогресса →

TypeScript: Структурная типизация

В JavaScript возможно работать с объектами и классами одинаковым образом. При этом не нужно опираться ни на наследование, ни на интерфейсы. Нужны только ожидаемые поля и методы. Такой подход называют утиной типизацией — duck typing. То что ходит как утка и крякает как утка – утка:

const user = {
  firstName: 'Vasiliy',
  lastName: 'Kuzenkov',
  type: 'user'
}

const admin = {
  firstName: 'Kirill',
  lastName: 'Mokevnin',
  type: 'admin'
}

const formatUser = (user) => [user.type, ':', user.firstName, user.lastName].join(' ');

formatUser(user); // ok
formatUser(admin); // ok

В языках как Java нам бы потребовалось определить интерфейс, после отдельно имплементировать его для классов User и Admin. А в параметрах метода форматирования тип аргумента был бы этим интерфейсом.

Другой вариант — написать метод с перегрузкой для этих двух случаев. Языки с таким поведением используют номинативную типизацию — nominative typing.

Чтобы организовать подход утиной типизации в Java, нужно написать много дополнительного кода.

Чтобы упростить переход с JavaScript на TypeScript и использовать проверки до выполнения кода, был выбран подход структурной типизации. С ней мы и познакомимся в этой уроке.

С помощью структурной типизации мы можем легко переписать наш пример на TypeScript:

const user = {
  firstName: 'Vassiliy',
  lastName: 'Kuzenkov',
  type: 'user'
}

const admin = {
  firstName: 'Kirill',
  lastName: 'Mokevnin',
  type: 'admin'
}

type User = {
  type: string,
  firstName: string,
  lastName: string
}

const formatUser = (user: User): string =>
  [user.type, ':', user.firstName, user.lastName].join(' ');

formatUser(user); // ok
formatUser(admin); // ok

Мы создали тип User, который описывает ожидаемую структуру объекта. При этом в функции formatUser мы указали, что ожидаемый аргумент должен соответствовать типу User. Таким образом функция formatUser принимает только объекты, которые содержат все поля из объектного типа User.

Важно помнить, что структурная типизация не защищает нас от наличия дополнительных полей в объекте:

const moderator = {
  firstName: 'Danil',
  lastName: 'Polovinkin',
  type: 'moderator',
  email: 'danil@polovinkin.com'
}

type User = {
  type: string,
  firstName: string,
  lastName: string
}

const formatUser = (user: User): string =>
  [user.type, ':', user.firstName, user.lastName].join(' ');

formatUser(moderator); // ok

При том что мы не указали поле email в типе User, TypeScript все равно не выдаст ошибку, так как в объекте moderator есть все поля, которые описаны в типе User.

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

object

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

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

При объединении | мы расширяем тип — увеличиваем число допустимых значений для типа. А при пересечении & — сужаем. Так мы уменьшаем число допустимых значений:

type IntersectionUser = {
  username: string;
  password: string;
} & {
    type: string;
}

const admin: IntersectionUser = {  // требуется совпадение c объектным типом и слева и справа от оператора &
  username: 'test',
  password: 'test',
  type: 'admin'
}

type UnionUser = {
    username: string;
    password: string;
} | {
    type: string;
}

const user: UnionUser = { username: 'test', type: 'user' } // достаточно совпадения с одним из объектных типов

Получившийся тип IntersectionUser описывает объекты, которые содержат поля username, password и type. А тип UnionUser — объекты, которые содержат поля username и password ИЛИ type.

object intersection

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

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

Ответ Когда при пересечении объектных типов встречаются поля с одинаковыми именами, то в результате типы этих полей будут также пересечены, и итоговый тип будет never.

При использовании объединенных типов в функциях нужно учитывать следующий момент. Рассмотрим пример:

const user: UnionUser = { username: 'test', type: 'user' };

const func = (user: UnionUser) => {
  console.log(user.type);
};

Компилятор выдаст ошибку, так как тип в переменной user может относиться либо к левому типу, либо к правому. В итоге нет гарантии, что в объекте user будет любое из свойст.

Задание

Опишите тип состояния DataState и перечисление LoadingStatus. Затем реализуйте функцию handleData(), которая принимает на вход DataState и возвращает строку в зависимости от состояния: loading... при LoadingStatus.loading, error при LoadingStatus.error, строку из числового поля data при LoadingStatus.success. Если статус не входит в перечисление, функция возвращает unknown.

const loading: DataState = { status: LoadingStatus.Loading };
console.log(handleData(loading)); // loading...

const error: DataState = { status: LoadingStatus.Error, error: new Error('error') };
console.log(handleData(error)); // error

const success: DataState = { status: LoadingStatus.Success, data: 42 };
console.log(handleData(success)); // 42
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

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

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное

Определения

  • Структурная типизация — принцип, который определяет совместимость типов на основе их описания (структуры). Переменная типа A также может использоваться в том месте, где ожидается тип B, если обладает той же или более широкой структурой.


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе