Теория языков программирования
Субтипирование и структурная типизация
TypeScript и Go выбрали структурную типизацию - и за это их критикуют сторонники Haskell. Java и C# выбрали номинальную - и за это критикуют разработчики TypeScript. Барбара Лисков в 1987 году сформулировала принцип, который неожиданно оказался применим ко всем этим системам - и нарушается в каждой из них.
- **TypeScript**: структурная типизация позволяет передавать объекты из разных библиотек без адаптеров - если структура совпадает, типы совместимы
- **Go interfaces**: любой тип автоматически реализует интерфейс при наличии нужных методов - это основа composability в экосистеме Go (io.Reader, io.Writer)
- **TypeScript exhaustive checks**: discriminated unions + never в default-ветке switch гарантируют, что добавление нового варианта типа требует обновления всех switch - компилятор как линтер для полноты обработки
Nominal vs Structural
Существует два принципиально разных ответа на вопрос «является ли тип A подтипом типа B?». **Номинальная** типизация отвечает: только если это явно объявлено (`A extends B` или `A implements B`). **Структурная** типизация отвечает: если A имеет хотя бы все члены B с совместимыми типами - неважно, как они называются и откуда. Java и C# используют номинальную; TypeScript, Go и OCaml - структурную (duck typing на уровне системы типов).
Структурная типизация иногда называется **утиной типизацией** на уровне компилятора. Если тип 'крякает как утка' (имеет нужные методы и поля), он совместим - без явного объявления. TypeScript намеренно выбрал структурную типизацию, потому что JavaScript сам по себе структурный.
В TypeScript: `type A = { x: number; y: number }` и `type B = { x: number }`. Является ли значение типа A присваиваемым переменной типа B?
Liskov Substitution Principle
Барбара Лисков сформулировала принцип в 1987 году: если S - подтип T, то объекты типа T можно заменить объектами типа S без изменения корректности программы. Это сильнее, чем просто 'S имеет все методы T' - требования включают ограничения на поведение: предусловия не должны усиливаться, постусловия не должны ослабляться, инварианты должны сохраняться. Классический нарушитель LSP - квадрат как подтип прямоугольника.
LSP нарушается чаще через поведенческие, а не структурные несоответствия. Тип может иметь все нужные методы, но семантика метода нарушает контракт суперкласса. Статическая система типов не может полностью проверить LSP - только тесты и дизайн-ревью.
Почему `Square extends Rectangle` нарушает LSP, даже если у Square есть все методы Rectangle?
Width and Depth Subtyping
Субтипирование записей (record types) работает по двум ортогональным правилам. **Ширина** (width): подтип может иметь больше полей - `{ x, y, z }` является подтипом `{ x, y }`. **Глубина** (depth): подтип может иметь более узкие типы полей - `{ name: 'Alice' }` является подтипом `{ name: string }`. Оба правила есть в TypeScript. Но для изменяемых полей depth subtyping нарушает типобезопасность - это ловушка для начинающих.
Depth subtyping безопасен только для **ковариантных** позиций (чтение). Для изменяемых полей (запись) нужна инвариантность. Для функций: аргументы контравариантны (подтип принимает более широкий аргумент), возвращаемые значения ковариантны. TypeScript упрощает правила за счёт soundness в некоторых случаях с массивами.
В TypeScript `string[]` присваивается `readonly string[]`. Верно ли обратное: `readonly string[]` присваивается `string[]`?
Flow Typing
**Flow typing** (или type narrowing) - автоматическое сужение типа переменной на основе анализа потока управления. После `if (x instanceof Error)` компилятор знает, что в этой ветке x имеет тип Error. После `if (x !== null)` - что x не null. TypeScript реализует это через discriminant unions и type guards. Kotlin называет это 'smart casts'. Это позволяет писать безопасный код без явных каст-операторов.
Flow typing наиболее мощен в сочетании с **discriminated unions** (tagged unions). Когда union-тип имеет общее поле с литеральным типом (discriminant), TypeScript автоматически сужает тип в каждой ветке `switch`. Это безопасная альтернатива `instanceof` для сложных иерархий типов.
Структурная типизация - это то же самое, что duck typing в Python или JavaScript
Структурная типизация проверяется **компилятором** на этапе компиляции. Duck typing в динамических языках - проверка во время выполнения с RuntimeError при ошибке. Структурная типизация даёт статические гарантии при той же гибкости.
TypeScript намеренно выбрал структурную типизацию для совместимости с JavaScript, где объекты создаются без классов - но добавил статическую проверку компилятором.
Что происходит с типом переменной `s` в ветке `case 'circle':` при switch по `s.kind`, если `s: Shape` и Shape - discriminated union?
Ключевые идеи
- **Номинальная типизация** требует явного объявления совместимости; **структурная** проверяет только наличие нужных членов - TypeScript, Go, OCaml используют структурную
- **LSP** сильнее структурной совместимости: подтип не должен ослаблять постусловия или нарушать инварианты - квадрат/прямоугольник классический нарушитель
- **Flow typing** автоматически сужает тип переменной через анализ потока управления - discriminated unions + exhaustive check = безопасность без каст-операторов
Связанные темы
Субтипирование пронизывает всю теорию типов и практику ООП:
- Алгебраические типы данных — Discriminated unions в TypeScript - это sum types, реализованные через структурную типизацию с discriminant полем
- Типы-классы и трейты — Трейты Rust - альтернатива субтипированию: вместо иерархии типов используются независимые контракты поведения
Вопросы для размышления
- Почему глубинное (depth) субтипирование для изменяемых полей нарушает типобезопасность, а для readonly - нет?
- Если бы Go использовал номинальную типизацию, как бы это изменило экосистему стандартных интерфейсов (io.Reader, io.Writer)?
- Можно ли средствами статической типизации полностью проверить соответствие принципу Лисков?
Связанные уроки
- plt-08-generics — Variance (co/contravariance) is the intersection of subtyping and generics - needs both
- plt-11-typeclasses — Typeclass polymorphism vs subtype polymorphism are alternative approaches to open extension
- plt-19-oop-theory — Subtyping is the theoretical foundation of OOP polymorphism; Liskov substitution principle formalizes it
- plt-02-type-systems — Subtyping is a relation in the type system; need the base type system framework
- fl-03-grammars