В 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
.
В структурной типизации об объектном типе можно думать, как об описании структуры, которое накладывает ограничения на присваиваемые значения. Или как о множестве объектов, которые могут быть присвоены переменной с таким типом.
Чем меньше полей в объектном типе, тем менее специфичное ограничений накладывается на присваиваемое значение. На множествах это означает, что объектный тип с дополнительными полями будет подмножеством объектного типа без этих полей. Если говорить о сужении и расширении типа в объектных типах, то дополнительные поля сужают тип.
Аналогично операциям со множествами для объектных типов можно сформировать понимание пересечения и объединения в структурной типизации.
При объединении |
мы расширяем тип — увеличиваем число допустимых значений для типа. А при пересечении &
— сужаем. Так мы уменьшаем число допустимых значений:
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
.
В алгебре множеств пересечение двух множеств также называют логическим умножением. А для типов используют термин произведение типов. Объединение множеств же называют логическим сложением, а для типов — сумма типов.
Попробуйте ответить, что будет, если использовать в пересечении два объектных типа с одинаковым именем поля, но с отличающимися типами. Это распространенная ошибка по невнимательности или из-за недостаточного понимания типов как множеств.
При использовании объединенных типов в функциях нужно учитывать следующий момент. Рассмотрим пример:
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
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1import handleData, { DataState, LoadingStatus } from './index';
2
3test('handleData', () => {
4 const loading: DataState = { status: LoadingStatus.Loading };
5 expect(handleData(loading)).toBe('loading...');
6
7 const success: DataState = { status: LoadingStatus.Success, data: 42 };
8 expect(handleData(success)).toBe('42');
9
10 const error: DataState = { status: LoadingStatus.Error, error: new Error('error') };
11 expect(handleData(error)).toBe('error');
12
13 const unknown = { status: 'unknown' };
14 // @ts-expect-error type '{ status: 'unknown' }' is not assignable to type 'DataState'.
15 expect(handleData(unknown)).toBe('unknown');
16});
17
Решение учителя откроется через: