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.
В структурной типизации об объектном типе можно думать, как об описании структуры, которое накладывает ограничения на присваиваемые значения. Или как о множестве объектов, которые могут быть присвоены переменной с таким типом.

Чем меньше полей в объектном типе, тем менее специфичное ограничение накладывается на присваиваемое значение. На множествах это означает, что объектный тип с дополнительными полями будет подмножеством объектного типа без этих полей. Если говорить о сужении и расширении типа в объектных типах, то дополнительные поля сужают тип.
Аналогично операциям со множествами для объектных типов можно сформировать понимание пересечения и объединения в структурной типизации.
При объединении | мы расширяем тип — увеличиваем число допустимых значений для типа. А при пересечении & — сужаем. Так мы уменьшаем число допустимых значений:
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.

В алгебре множеств пересечение двух множеств также называют логическим умножением. А для типов используют термин произведение типов. Объединение множеств же называют логическим сложением, а для типов — сумма типов.
Попробуйте ответить, что будет, если использовать в пересечении два объектных типа с одинаковым именем поля, но с отличающимися типами. Это распространенная ошибка по невнимательности или из-за недостаточного понимания типов как множеств.
Ответ
Когда при пересечении объектных типов встречаются поля с одинаковыми именами, то в результате типы этих полей будут также пересечены, и итоговый тип будет 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Полезное
Ваше упражнение проверяется по этим тестам
import { expect, test } from 'vitest';
import handleData, { type DataState, LoadingStatus } from './index';
test('handleData', () => {
const loading: DataState = { status: LoadingStatus.Loading };
expect(handleData(loading)).toBe('loading...');
const success: DataState = { status: LoadingStatus.Success, data: 42 };
expect(handleData(success)).toBe('42');
const error: DataState = {
status: LoadingStatus.Error,
error: new Error('error'),
};
expect(handleData(error)).toBe('error');
const unknown = { status: 'unknown' };
// @ts-expect-error type '{ status: 'unknown' }' is not assignable to type 'DataState'.
expect(handleData(unknown)).toBe('unknown');
});Решение учителя откроется через:
20:00
