Бесплатный курс по 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 }): string =>
  [user.type, ':', user.firstName, user.lastName].join(' ');

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

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

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

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

formatUser(moderator); // ok

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

object

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

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

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

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

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

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

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

object intersection

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

Ответ При объединении объектных типов, если встречаются поля с одинаковыми именами, то они должны быть совместимы. То есть иметь одинаковый тип. Иначе будет ошибка компиляции, так как итоговый тип будет `never`.

Задание

Опишите тип состояния 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
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе