Теория языков программирования

Gradual Typing: TypeScript и друзья

Microsoft мигрировала 20 млн строк JavaScript на TypeScript - и нашла тысячи скрытых багов. Google перевела 20% своего Python-кода на строгие type hints. Airbnb сократила runtime-ошибки на 38% после внедрения TypeScript. Но ни одна из этих компаний не остановила разработку ради миграции. Как? Gradual typing - добавляй типы постепенно, файл за файлом.

  • **Миграция крупных проектов**: JS → TS без остановки разработки (Microsoft, Google, Airbnb, Slack)
  • **Python-экосистема**: FastAPI использует type hints для автоматической валидации API запросов
  • **IDE superpowers**: автокомплит, переход к определению, рефакторинг - всё благодаря типам
  • **Документация как код**: аннотации типов - это документация, которая проверяется компилятором и никогда не устаревает

Gradual Typing - типы по запросу

Представьте: вы делаете ремонт в квартире. Можно выехать, снести всё до бетона и сделать заново за полгода. А можно ремонтировать по комнатам - живёте в квартире и постепенно обновляете. **Gradual typing** - это ремонт по комнатам для кода.

**Gradual typing** (постепенная типизация) - подход, при котором типы добавляются в программу опционально. Код без типов продолжает работать, а типизированные участки проверяются компилятором. Нет необходимости переписывать всё сразу.

Рождение идеи gradual typing

Jeremy Siek и Walid Taha в 2006 году формализовали идею gradual typing в статье «Gradual Typing for Functional Languages». Они показали, что можно создать единую систему, где статически типизированный и динамически типизированный код сосуществуют безопасно. Эта идея стала фундаментом для TypeScript, Python type hints и многих других систем.

Ключевое отличие от обычной статической типизации: тип **`any`** (или его аналоги) служит границей между типизированным и нетипизированным мирами. Всё, что помечено `any`, компилятор не проверяет - доверяет программисту.

ПодходСтратегияПримерРиск
Полная статикаВсе типы обязательны с самого началаJava, C#, RustМедленный старт, много boilerplate
Полная динамикаТипов нет, всё проверяется в runtimeJavaScript, Python, RubyRuntime ошибки, сложный рефакторинг
Gradual typingТипы опциональны, добавляй постепенноTypeScript, Python+mypy, HackUnsoundness через any/границы

**Миграция JS → TS**: переименуйте `.js` в `.ts` - любой JavaScript уже является валидным TypeScript. Файл за файлом добавляйте аннотации типов, начиная с самых критичных модулей (API-границы, бизнес-логика).

Что делает gradual typing принципиально отличным от обычной статической типизации?

TypeScript - самая успешная gradual type system

TypeScript - не просто «JavaScript с типами». Это полноценная **структурная** система типов с union types, narrowing, mapped types и conditional types. Разберём ключевые механизмы, которые делают TypeScript выразительным инструментом.

**Structural typing** означает, что TypeScript сравнивает типы по **структуре** (какие поля есть), а не по **имени** (как класс называется). Если объект имеет все нужные поля - он подходит, неважно как он был создан.

tsconfig опцияЧто проверяетКогда включать
strict: falseМинимальные проверкиНачало миграции с JS
noImplicitAnyЗапрещает неявный anyПервый шаг к строгости
strictNullChecksnull/undefined требуют проверкиВторой шаг - ловит NullPointerError
strictFunctionTypesПроверка контравариантности параметровПродвинутый этап
strict: trueВсе проверки включеныЦель миграции

**`unknown` вместо `any`**: тип `unknown` безопаснее - он требует проверки перед использованием. Используйте `any` только как временное решение при миграции, а `unknown` - для внешних данных (API, JSON, пользовательский ввод).

Почему TypeScript использует structural typing, а не nominal (как Java)?

Python Type Hints - типы без принуждения

Python пошёл ещё дальше TypeScript в философии «типы опциональны». В TypeScript компилятор хотя бы проверяет типы перед генерацией JS. В Python аннотации типов **вообще не влияют на выполнение программы** - это просто метаданные. Проверка происходит отдельными инструментами: mypy, pyright, pytype.

ИнструментАвторОсобенности
mypyJukka Lehtosalo (Dropbox)Первый и самый популярный, строгий режим через --strict
pyrightMicrosoftБыстрый (на TypeScript!), встроен в VS Code через Pylance
pytypeGoogleУмеет выводить типы из кода без аннотаций
pyreMeta (Facebook)Инкрементальная проверка для огромных кодовых баз

**Ключевое отличие от TypeScript**: в Python аннотации типов - это **только метаданные**. Интерпретатор Python их полностью игнорирует. Но фреймворки вроде FastAPI/Pydantic читают аннотации через `inspect` модуль и используют для runtime-валидации - это мост между gradual typing и реальной безопасностью.

**Проблема проникновения `Any`**: если хоть одна функция возвращает `Any`, он «заражает» всё, что с ней взаимодействует. mypy перестаёт проверять типы в цепочке вызовов. Включайте `--disallow-any-generics` и `--warn-return-any` для раннего обнаружения.

Что произойдёт при выполнении Python-кода `def add(x: int, y: int) -> int: return x + y; add("hello", "world")`?

Soundness: почему TypeScript намеренно "дырявый"

TypeScript team открыто признаёт: их система типов **unsound**. Это не баг - это осознанное проектное решение. Sound type system гарантирует, что если код скомпилировался, runtime type errors невозможны. TypeScript такой гарантии не даёт. Но почему это оказалось правильным выбором?

ЯзыкSoundnessУдобствоAdoptionФилософия
TypeScriptUnsound (намеренно)ВысокоеОгромноеПрагматизм > чистота
FlowБлиже к soundСреднееПадаетБольше академичности
HaskellSoundКрутая кривая обученияНишевоеКорректность прежде всего
RustSoundВысокое (после обучения)РастётZero-cost abstractions
Python + mypyUnsoundВысокоеРастётОпциональные типы - ваш выбор

Почему unsound TypeScript победил sound Flow? Ответ в **кривой adoption**. TypeScript позволяет постепенную миграцию: любой JS-файл можно переименовать в .ts и он скомпилируется. Flow требовал больше изменений, его инструменты работали медленнее, а сообщество росло медленнее. Прагматизм победил академическую чистоту.

  • Sound System (Rust, Haskell) — Если скомпилировалось - гарантия отсутствия type errors в runtime. Цена: больше boilerplate, сложнее FFI с нетипизированным кодом, крутая кривая обучения.
  • Unsound System (TypeScript, Python+mypy) — Компиляция проходит - но runtime errors возможны через escape hatches (any, as, assertions). Цена: нет 100% гарантии, но зато лёгкая миграция и низкий порог входа.

**Правило для production-кода**: Не доверяйте данным, пересекающим границу вашей системы - API ответы, пользовательский ввод, файлы, переменные окружения. Валидируйте на границах с помощью zod/io-ts/class-validator, и unsoundness TypeScript перестанет быть проблемой.

Почему разработчики TypeScript НАМЕРЕННО сделали систему типов unsound?

Итоги

  • **Gradual typing** - философия постепенного добавления типов: код без типов работает, типы добавляются по необходимости
  • **TypeScript** - структурная система типов с union types, narrowing и выразительным type inference; unsound по дизайну
  • **Python type hints** - аннотации не влияют на runtime; проверяются внешними инструментами (mypy, pyright)
  • **Soundness trade-off** - TypeScript намеренно unsound ради совместимости с JS и лёгкой миграции
  • **Защита на границах** - валидация внешних данных (zod, Pydantic) компенсирует unsoundness в production

Gradual Typing в контексте теории языков

Gradual typing связывает статические и динамические миры, создавая мост между ними.

  • Structural vs Nominal Typing — TypeScript использует structural typing - объекты сравниваются по структуре, а не по имени
  • Type Inference — Hindley-Milner inference в Haskell vs локальный inference в TypeScript - разные подходы к выводу типов
  • Dependent Types — Следующий рубеж: типы, зависящие от значений (Idris, Agda) - полная верификация программ
  • Runtime Validation — Zod, io-ts, Pydantic - мост между compile-time типами и runtime-безопасностью

Вопросы для размышления

  • В вашем проекте есть `any`? Сколько? Можно ли заменить на конкретные типы или `unknown`?
  • Как вы обрабатываете данные из внешних API - доверяете структуре или валидируете?
  • Если бы вы начинали новый проект, выбрали бы TypeScript strict с первого дня или добавляли типы постепенно?

Связанные уроки

  • plt-03-static-vs-dynamic — Gradual typing bridges static and dynamic - need to understand both ends first
  • plt-04-type-inference — Type inference makes gradual typing practical by reducing annotation burden
  • plt-07-algebraic-types — Once gradual typing is mastered, richer type constructs like ADTs become the next step
  • plt-02-type-systems — Foundation: what a type system is and what guarantees it provides
  • comp-01-intro
Gradual Typing: TypeScript и друзья

0

1

Войти