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

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

В JavaScript мы можем работать с различными объектами и классами одинаковым образом, при этом нам не требуется опираться на наследование или определенные интерфейсы, достаточно наличия ожидаемых полей и методов. В мире такой подход называют утиной типизацией (Duck typing). Если что-то ходит как утка и крякает как утка, то это утка.

const user = {
  firstName: 'Vassiliy',
  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'
}

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

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

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

const moderator = {
  firstName: 'Danil',
  lastName: 'Polovinkin',
  type: 'moderator'
}

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

formatUser(moderator); // ok

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

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

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

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

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

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

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

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

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

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

const admin: IntersectionUser = { username: 'test', password: 'test', type: 'admin' }
//                                                                    ~~~~

Задание

Упражнение не проходит проверку — что делать? 😶

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

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

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

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

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

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

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

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

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

Полезное

Определения

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


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