ПрограммированиеTypeScript архитектор

В чем разница между структурной (структурной) и номинативной типизацией в TypeScript? Можно ли реализовать номинативную типизацию, если можно — как? Какие проблемы это может решить?

Проходите собеседования с ИИ помощником Hintsage

Ответ

TypeScript использует структурную типизацию (structural typing), или "типизацию по утиной типизации". Для совместимости типов важна структура (сигнатура), а не имя или происхождение типа.

Пример:

interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // Эти типы взаимозаменяемы: Point2D и Coord2D, потому что структура одинаковая. const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!

Номинативная типизация (nominal typing): для совместимости типов важно "имя" или "фабрика", структуру неважна.

В TypeScript номинативная типизация штатно не поддерживается, но можно эмулировать её с помощью branded types:

type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // Ошибка! Разные бренды.

Зачем это применять? Например, чтобы различать одинаковые по структуре, но смыслово разные типы — валюты, userID/tokenID, физические величины и т.д.


Вопрос с подвохом

Вопрос: Почему следующий код компилируется без ошибок, хотя Address и UserId логически разные типы?

interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK

Ответ: Потому что в TypeScript важна структура, а не имя типа. Оба типа — просто "объект с value: string".


Примеры реальных ошибок из-за незнания тонкостей темы


История

Проект: Финансовая система с расчетами в USD/EUR. Суммы передавались через number. Однажды перепутали валюты при сложении — из-за структурной типизации TypeScript это не обнаружил. Позже ввели branded types, чтобы исключить такие ошибки на этапе компиляции.


История

Проект: В разработке REST API использовали объекты для Id-шников разных сущностей (userId, groupId), обеих с полем value: string. По ошибке userId подставлялся вместо groupId, и только бизнес-логика на сервере обнаруживала ошибку.


История

Проект: В библиотеке парсеров для DSL использовали одинаковые структуры (type value = { kind: 'num'|'str', value: number|string }). Однотипные структуры перемешались между разными частями кода, что приводило к логическим ошибкам. Добавили искусственные brand-поля для разделения.