Номинальная vs структурная типизация
Два подхода к проверке совместимости типов. Номинальная — тип определяется именем (C++, Java, C#). Структурная — тип определяется набором полей и их типами (TypeScript, Go). JavaScript на лету структурный (duck typing).
«TypeScript поддерживает такую же самую структурную типизацию, как и весь JavaScript — потому что у нас такая культура совместимости», y-dWNQ7RCw4.
Номинальная типизация (C++, Java, C#)
// Два разных типа с одинаковой структурой
struct UserId { std::string value; };
struct OrderId { std::string value; };
void process(UserId id) { /* ... */ }
OrderId oid{"abc"};
process(oid); // ❌ COMPILE ERROR: cannot convert OrderId to UserId
- Тип определяется именем, не структурой
- Невозможно перепутать
UserIdиOrderId, даже если поля идентичны - Строгая защита от логических ошибок
Структурная типизация (TypeScript)
type User = { name: string };
type Person = { name: string };
const getName = (x: User | Person) => x.name;
const u: User = { name: 'Ivan' };
const p: Person = { name: 'Anna' };
getName(u); // OK
getName(p); // OK
getName({ name: 'X' }); // OK — структурно подходит
- Тип определяется набором полей и их типами
UserиPersonсовместимы — обе структуры сname: string- Истоки в duck typing: «есть
name: string— годится»
Что остаётся в runtime JS
// До компиляции
type UserId = { value: string };
const uid: UserId = { value: 'u1' };
// После компиляции в JS
const uid = { value: 'u1' };
// Типы пропали полностью
В TypeScript типы стираются на этапе компиляции. В runtime — обычный JavaScript, никакой проверки нет.
Номинальная типизация на голом JS — через Symbol brand
Если хочется различать UserId и OrderId в runtime, придётся брендировать через символы:
const USER_ID = Symbol('userId');
const ORDER_ID = Symbol('orderId');
const createUserId = value => Object.freeze({ value, [USER_ID]: true });
const createOrderId = value => Object.freeze({ value, [ORDER_ID]: true });
const getUserId = obj => {
if (!obj[USER_ID]) throw new TypeError('expected UserId');
return obj.value;
};
const uid = createUserId('u1');
const oid = createOrderId('o1');
getUserId(uid); // 'u1' ✓
getUserId(oid); // TypeError: expected UserId
- Символы userID и orderID — бренды
Object.freezeзащищает бренд от удаления- Фабрики
createUserIdединственный способ получить «настоящий» UserId - Проверка
obj[USER_ID]отсеивает чужие объекты
Branded types в TypeScript
В TS брендирование делают через intersection с пустым типом:
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };
const makeUserId = (s: string): UserId => s as UserId;
const makeOrderId = (s: string): OrderId => s as OrderId;
function getUser(id: UserId) { /* ... */ }
const oid = makeOrderId('o1');
// getUser(oid); // ❌ Compile error — но в runtime защиты нет
Это compile-time only. Защита от программистских ошибок, не от вредоносного кода.
Зачем номинальные типы
- Безопасность ID — нельзя перепутать
UserIdиOrderId, даже если оба строки - Brand для денег —
USDиEURне складываются - Валидация —
EmailStringгарантирует, что строка прошла регулярку
Инкапсуляция vs сокрытие
В этой же лекции автор различает:
- Инкапсуляция — объединение полей и поведения в одну абстракцию (структурный принцип ООП)
- Сокрытие — невозможность извне дотянуться до внутренностей (механизм защиты)
«Сокрытие — это в итоге всегда
if. Если вы где-то пишетеprivateилиprotected, это всё равно где-то происходитif. Просто от вас немножко скрыто».
В JS приватность достигается через:
#name(ES2022) — настоящие приватные поля- замыкания — переменная вне области видимости
- WeakMap — внешняя таблица
_underscore— соглашение, не защита
Источники
- Timur · Номинальная и структурная типизация, инкапсуляция, сокрытие (2025-11-01)
- Timur · ООП построение абстракций, инкапсуляция и сокрытие (2020-03-03)