TypeScript: Практический курс типизации для JavaScript

Курс знакомит с TypeScript и его ключевыми возможностями: статической типизацией, интерфейсами, дженериками и современными фичами языка. Вы научитесь настраивать проект, писать безопасный код и применять TypeScript в реальных приложениях и экосистеме JavaScript.

1. Введение в TypeScript и преимущества типизации

Введение в TypeScript и преимущества типизации

TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию и несколько удобных возможностей для разработки. При этом TypeScript не заменяет JavaScript: итоговый код, который выполняется в браузере или Node.js, всё равно является JavaScript.

В этом курсе мы будем учиться писать привычный JavaScript-код, но с типами: так, чтобы редактор и компилятор заранее находили ошибки, помогали с автодополнением и делали изменения в коде безопаснее.

Что такое TypeScript

TypeScript — язык программирования от Microsoft, который:

  • является надмножеством JavaScript: почти любой валидный JavaScript — это валидный TypeScript
  • добавляет систему типов (аннотации, вывод типов, проверки)
  • компилируется в JavaScript (процесс часто называют транспиляцией)
  • TypeScript не запускается напрямую в браузере. Его нужно преобразовать в JavaScript.

    !Схема показывает, что типы проверяются до запуска, а выполняется итоговый JavaScript.

    Полезные официальные источники:

  • Официальный сайт TypeScript
  • TypeScript Handbook
  • Зачем нужна типизация в JavaScript-проектах

    JavaScript — язык динамической типизации: тип значения известен только во время выполнения. Это даёт гибкость, но часть ошибок проявляется поздно — в рантайме, иногда уже у пользователя.

    TypeScript добавляет статическую (проверяемую заранее) модель типов. Это позволяет ловить целые классы ошибок ещё до запуска программы.

    Какие проблемы помогает решать TypeScript

  • Раннее обнаружение ошибок
  • Улучшение автодополнения и навигации по коду
  • Безопасный рефакторинг
  • Явный контракт между частями системы (функции, модули, API)
  • Рассмотрим на примере.

    Пример: ошибка, которая в JavaScript проявится только в рантайме

    Представим функцию, которая должна приветствовать пользователя.

    Проблема: при вызове greet(null) код упадёт, потому что у null нет свойства name.

    В TypeScript мы можем явно описать, что функция ожидает объект определённой формы.

    Здесь:

  • User — это тип, описывающий структуру объекта
  • user: User — аннотация типа параметра
  • : string — аннотация типа возвращаемого значения
  • Важно: TypeScript не делает ваш код «непадающим». Он помогает заранее увидеть небезопасные места и исправить их до выполнения.

    Статическая типизация простыми словами

    Статическая типизация означает, что инструмент (компилятор TypeScript) проверяет согласованность типов заранее.

    Например, если функция объявлена как принимающая строку, а вы передаёте число, TypeScript сообщит об этом при проверке.

    Важная идея: TypeScript — это постепенная типизация

    TypeScript поддерживает постепенную (gradual) типизацию: вы можете добавлять типы по мере необходимости.

    Это особенно важно для реальных проектов, где:

  • уже есть большая кодовая база на JavaScript
  • типизация внедряется постепенно
  • часть кода временно может быть «слабо типизирована», а часть — строго
  • TypeScript позволяет настраивать строгость через конфигурацию проекта.

    Что происходит с типами при компиляции

    Типы существуют только на этапе разработки. При компиляции TypeScript удаляет типовую информацию и выдаёт JavaScript.

    После компиляции получится JavaScript примерно такого вида:

    Это помогает понять ограничения TypeScript:

  • типы не «живут» в рантайме сами по себе
  • проверки типов не заменяют валидацию данных извне (например, данных из сети)
  • TypeScript улучшает опыт разработки

    TypeScript тесно интегрирован с редакторами кода (особенно с VS Code). Даже без запуска проекта вы получаете:

  • подсказки по параметрам функций
  • автодополнение свойств объекта
  • поиск всех мест использования
  • более безопасное переименование (refactor rename)
  • На практике это ускоряет разработку и снижает количество регрессий.

    Где TypeScript используется чаще всего

    TypeScript можно использовать почти везде, где используется JavaScript:

  • фронтенд-приложения (React, Vue, Angular)
  • Node.js-сервисы
  • библиотеки и SDK
  • монорепозитории с большим количеством пакетов
  • Популярность TypeScript связана с тем, что он масштабирует разработку: чем больше команда и кодовая база, тем больше ценность типов.

    Что будет дальше в курсе

    Дальше мы перейдём от общих идей к практике:

  • установим TypeScript и настроим компиляцию
  • разберём базовые типы (строки, числа, массивы, объекты)
  • научимся типизировать функции и работать с type и interface
  • рассмотрим типизацию реальных сценариев: данные API, обработчики событий, конфигурации
  • Для справки по конфигурации TypeScript пригодится официальная документация:

  • tsconfig.json (TypeScript Handbook)
  • 2. Базовые типы, аннотации и вывод типов

    Базовые типы, аннотации и вывод типов

    В прошлой статье мы разобрали, зачем TypeScript добавляет статическую типизацию в мир JavaScript и почему это снижает количество ошибок ещё до запуска кода. Теперь перейдём к практике: изучим базовые типы TypeScript, научимся задавать типы через аннотации и поймём, когда TypeScript может вывести тип автоматически.

    !Схема показывает два источника информации о типах: явные аннотации и автоматический вывод типов.

    Что такое тип, аннотация и вывод типов

    Тип в TypeScript — это описание того, какие значения допустимы.

  • string описывает строки
  • number описывает числа
  • { name: string } описывает объект с полем name, которое является строкой
  • Аннотация типа — это явная подсказка TypeScript в коде.

    Вывод типов (type inference) — это когда TypeScript сам определяет тип по присваиванию и использованию.

    Практическое правило: если тип очевиден из значения — можно не писать аннотацию, а если это граница модуля или публичный контракт (например, параметры функции) — аннотация часто полезна.

    Официальная документация:

  • Everyday Types (TypeScript Handbook)
  • Type Inference (TypeScript Handbook)
  • Примитивные типы

    string

    В TypeScript строка — это string. Методы вроде toUpperCase() будут доступны, а опечатки в названии метода редактор подсветит.

    number

    В JavaScript нет отдельных типов для целых и дробных чисел, поэтому TypeScript использует единый тип number.

    boolean

    bigint

    bigint нужен для очень больших целых чисел.

    Важно: bigint и number — разные типы, их нельзя свободно смешивать.

    symbol

    symbol используется для уникальных идентификаторов.

    null и undefined

    В JavaScript есть два «пустых» значения:

  • undefined — значение отсутствует
  • null — значение явно задано как «ничего»
  • TypeScript может быть настроен по-разному, но в большинстве проектов включают строгую проверку null и undefined (опция strictNullChecks). Тогда, например, string и string | null — это разные типы.

    string | null читается так: строка или null.

    Типы объектов

    Самый простой способ описать объект — указать структуру прямо в типе.

    Необязательные свойства

    Если свойство может отсутствовать, ставят ?.

    Когда вы обращаетесь к необязательному свойству, TypeScript будет напоминать, что оно может быть undefined.

    Только для чтения

    readonly запрещает менять свойство после создания объекта.

    Массивы

    Есть две равнозначные записи для массива строк:

    Обычно string[] проще читать.

    TypeScript будет контролировать элементы:

    Кортежи

    Кортеж (tuple) — это массив фиксированной длины, где тип задан для каждой позиции.

    Это удобно, когда позиция элемента имеет смысл (например, координаты, результат парсинга, пара ключ-значение). Если структура растёт и становится сложной, часто лучше перейти на объект.

    Литеральные типы

    Иногда важно, чтобы значение было не просто string, а конкретной строкой.

    Литеральные типы часто используют вместе с объединениями.

    Объединения (union types) и сужение типов

    Объединение типов записывается через |.

    Проблема: у number и string разные методы. Чтобы безопасно использовать специфичные методы, TypeScript требует сужения типа (narrowing) — то есть проверки, после которой становится понятно, какой тип сейчас.

    Функции: типы параметров и результата

    Аннотации особенно полезны на границах функций.

    Если не указать тип возвращаемого значения, TypeScript часто выведет его автоматически:

    void

    void означает, что функция ничего не возвращает (или возвращаемое значение игнорируется).

    never

    never означает, что выполнение никогда не дойдёт до конца функции.

    Типичные случаи:

  • функция всегда выбрасывает ошибку
  • функция содержит бесконечный цикл
  • never полезен как сигнал: эта ветка кода невозможна или всегда прерывает выполнение.

    any и unknown

    any

    any отключает проверку типов для значения.

    any иногда нужен при миграции с JavaScript или при работе с плохо типизированными библиотеками, но его лучше избегать: он ломает главную пользу TypeScript.

    unknown

    unknown означает: тип неизвестен, но в отличие от any он заставляет вас сделать проверку перед использованием.

    unknown часто полезнее any, когда данные приходят извне (сеть, файлы, пользовательский ввод): вы явно подтверждаете проверки.

    Когда писать аннотации, а когда полагаться на вывод типов

    Практичный подход выглядит так:

  • Пишите аннотации для параметров функций, публичных API, сложных структур данных.
  • Полагайтесь на вывод типов для локальных переменных, где всё очевидно из присваивания.
  • Избегайте any, предпочитайте unknown + проверки.
  • Если включён строгий режим TypeScript, ошибки с null/undefined будут выявляться раньше, но код станет безопаснее.
  • Итог

    В этой статье мы познакомились с базовыми строительными блоками TypeScript:

  • примитивные типы (string, number, boolean, bigint, symbol)
  • null и undefined и почему их важно учитывать
  • типы объектов, необязательные и readonly свойства
  • массивы и кортежи
  • объединения и сужение типов
  • основы типизации функций, void и never
  • отличие any от unknown
  • Дальше эти знания станут основой для более удобной и масштабируемой типизации: мы начнём активно использовать собственные типы через type и interface, а также научимся описывать более сложные структуры данных.

    3. Функции, объекты, интерфейсы и type alias

    Функции, объекты, интерфейсы и type alias

    В предыдущих статьях мы разобрали базовые типы TypeScript, объединения, сужение типов и основы типизации функций. Теперь соберём эти знания в более практичную форму: научимся описывать контракты для функций и объектов так, как это делается в реальных проектах.

    Ключевая идея TypeScript в этой теме: вы описываете форму данных и форму API, а TypeScript проверяет, что ваш код соответствует этим ожиданиям.

    Официальные материалы по теме:

  • Everyday Types (TypeScript Handbook)
  • Functions (TypeScript Handbook)
  • Object Types (TypeScript Handbook)
  • !Схема: чем обычно отличаются interface и type alias и что их объединяет

    Типизация объектов: как описывать структуру данных

    Самая частая задача в TypeScript — описать объект: какие у него поля, какие из них обязательны, какие опциональны, какие доступны только для чтения.

    Объектный тип напрямую

    Здесь:

  • id: number и name: string — обязательные свойства
  • email?: string — опциональное свойство, оно может отсутствовать, и тогда его значение фактически будет undefined
  • Так можно делать в небольших местах кода, но для повторного использования удобнее выносить тип в отдельное имя.

    readonly: защита от случайных изменений

    readonly полезен для конфигураций, DTO-объектов (данных, которые вы получили извне) и любых сущностей, которые не должны мутироваться в процессе работы.

    Индексные сигнатуры: объект-словарь

    Иногда заранее неизвестны ключи, но известен тип значений.

    Такой тип читается как: любой строковый ключ разрешён, и по нему лежит строка.

    Функции: параметры, возвращаемое значение и типы функций

    TypeScript особенно полезен на границах функций: там, где данные входят в модуль и выходят из него.

    Аннотации параметров и результата

    Практическое правило:

  • типы параметров лучше указывать почти всегда
  • возвращаемый тип можно не указывать, если он очевиден, но для публичных функций часто полезно фиксировать его явно
  • Опциональные параметры и значения по умолчанию

    Опциональный параметр помечается ?.

    Параметр с значением по умолчанию не обязан быть передан.

    Важно: опциональность и значение по умолчанию решают похожие задачи, но смысл разный.

  • query?: string означает: параметр может отсутствовать, и тогда его значение будет undefined
  • times: number = 2 означает: если параметр не передан, используется 2
  • Rest-параметры

    Когда функция принимает переменное число аргументов, используйте rest-параметр.

    Тип функции как значение

    Функцию можно описать как тип и передавать как аргумент.

    Здесь Predicate<T> — это type alias для сигнатуры функции.

    Типизация колбэков

    Когда вы принимаете колбэк, важно типизировать:

  • параметры колбэка
  • возвращаемое значение колбэка
  • type alias: именуем типы и собираем сложные конструкции

    type alias — это способ дать имя любому типу: объекту, объединению, кортежу, литералу, функции.

    Объединения через type alias

    Это удобно для конечных наборов состояний, ролей, режимов работы.

    Пересечения (intersection) для композиции

    Пересечение записывается через & и означает: объект должен удовлетворять обоим типам одновременно.

    Композиция через & часто используется в доменных моделях и при сборке типов из «примесей».

    interface: контракт для объектов и классов

    interface — это способ описать форму объекта. Чаще всего интерфейсы используют для:

  • публичных контрактов модулей
  • описания объектов и API
  • описания того, что должен реализовать класс
  • Расширение интерфейсов через extends

    extends позволяет строить иерархию контрактов.

    Это удобно, когда есть базовая часть и специальные расширения.

    Declaration merging: объединение объявлений interface

    Особенность TypeScript: если объявить один и тот же interface несколько раз в одной области видимости, они будут объединены.

    На практике это важно при расширении глобальных типов или при типизации библиотек.

    interface vs type alias: что выбирать

    Оба инструмента решают похожую задачу: дать имя типу. Разница в возможностях и в стиле.

    Короткое правило выбора

  • Используйте interface, когда описываете объектный контракт и ожидаете расширение через extends или возможное объединение объявлений.
  • Используйте type, когда вам нужны union, intersection, кортежи, литералы или более сложные композиции.
  • Таблица отличий

    | Возможность | interface | type alias | |---|---|---| | Описание объектной структуры | да | да | | Объединения A \| B | нет | да | | Пересечения A & B | ограниченно через extends | да | | Кортежи, литералы, примитивные алиасы | нет | да | | Объединение объявлений (declaration merging) | да | нет | | Расширение | extends | через & (композиция) |

    Важно: в большинстве прикладных случаев можно выбрать любой подход, но консистентность внутри проекта важнее микровыбора.

    Структурная типизация: почему важна форма, а не имя

    TypeScript проверяет типы структурно: если объект имеет нужные свойства нужных типов, он подходит.

    Это делает TypeScript гибким: вам не нужно явно «наследоваться» или помечать объект специальным образом, достаточно совпадения формы.

    Excess property checks: «лишние поля» в литералах

    При этом есть важный нюанс: если вы передаёте объектный литерал напрямую, TypeScript выполняет дополнительную проверку на «лишние свойства».

    Смысл этой проверки — ловить опечатки и несоответствия именно там, где вы создаёте объект «на месте».

    Итог

    В этой статье мы научились описывать практические контракты TypeScript:

  • типизировать объекты: обязательные поля, опциональные поля, readonly, словари через индексные сигнатуры
  • типизировать функции: параметры, возврат, опциональные и rest-параметры, сигнатуры функций как типы
  • использовать type alias для именования типов, объединений, пересечений, литеральных типов и сигнатур функций
  • использовать interface для объектных контрактов, расширения через extends и объединения объявлений
  • понимать структурную типизацию и нюанс с excess property checks
  • Дальше эти инструменты станут базой для более выразительной типизации: работы с обобщениями (generics), типовыми утилитами и моделированием данных из API.

    4. Классы, модификаторы доступа и наследование

    Классы, модификаторы доступа и наследование

    В предыдущих материалах мы научились описывать контракты данных и API с помощью type и interface. Это основа типизации в TypeScript. Но в реальных проектах часто нужно описывать не только форму данных, но и поведение: методы, инварианты, правила изменения состояния. Для этого в TypeScript (как и в JavaScript) используются классы.

    Класс — это способ описать:

  • структуру объекта (поля)
  • поведение (методы)
  • правила доступа к внутреннему состоянию (модификаторы доступа)
  • переиспользование поведения через наследование
  • Официальные материалы:

  • TypeScript Handbook — Classes
  • TypeScript Handbook — Interfaces
  • MDN — Private class fields
  • !Схема показывает базовый класс и два наследника, а также идею переопределения методов

    Что такое класс и экземпляр

    Класс — это шаблон, по которому создаются объекты.

  • Экземпляр (instance) — конкретный объект, созданный из класса через new.
  • Конструктор — специальный метод constructor, который выполняется при создании экземпляра.
  • Пример простого класса:

    Ключевые идеи:

  • наследник обязан вызвать super(...) до обращения к this в конструкторе
  • ключевое слово override полезно тем, что TypeScript проверит: вы действительно переопределяете существующий метод, а не ошиблись в названии
  • super.method() позволяет использовать реализацию базового класса
  • protected в наследовании

    protected часто используют для деталей реализации, которые нужны наследникам, но не должны быть частью публичного API.

    Абстрактные классы: общий каркас без создания экземпляра

    Абстрактный класс нужен, когда вы хотите:

  • задать общий каркас и часть реализации
  • потребовать от наследников реализовать определённые методы
  • запретить создание экземпляров базового класса
  • Абстрактные классы полезны, когда есть общая реализация, которую хочется разделить между наследниками.

    implements: класс реализует интерфейс

    Если extends — это переиспользование реализации, то implements — это выполнение контракта.

    Так удобно отделять требования к API (интерфейс) от конкретной реализации (класс).

    Статические члены: static

    Статические свойства и методы принадлежат самому классу, а не экземплярам.

    Типичные случаи для static:

  • фабричные методы (fromJson, create)
  • счётчики, кеши, общие настройки
  • утилиты, связанные с конкретной сущностью
  • Как выбирать между class, interface и type

    В TypeScript нет универсального правила, но есть практичные ориентиры:

  • используйте interface и type, когда нужно описать форму данных и контракты между частями системы
  • используйте class, когда важны поведение, инкапсуляция, жизненный цикл объекта, управление состоянием
  • не усложняйте: если достаточно функции и объекта-данных, класс не обязателен
  • При этом помните идею из прошлых статей: TypeScript опирается на структурную типизацию. Это означает, что совместимость часто определяется формой. Однако модификаторы доступа (private/protected) влияют на совместимость экземпляров классов и помогают удерживать границы API.

    Итог

    В этой статье вы изучили, как TypeScript работает с объектно-ориентированными возможностями:

  • что такое класс, конструктор, экземпляр, поля и методы
  • как использовать модификаторы доступа public, private, protected и зачем они нужны
  • чем отличается private TypeScript от #private JavaScript
  • как применять readonly, геттеры и сеттеры для контроля доступа к данным
  • как работает наследование через extends, вызов super и переопределение методов с override
  • что такое абстрактные классы и как implements связывает класс с интерфейсом
  • зачем нужны статические (static) свойства и методы
  • Дальше эти знания станут полезны при построении более сложных моделей: типизации обобщённых классов, паттернов проектирования, а также при работе с типами библиотек, где классы и интерфейсы часто используются вместе.

    5. Дженерики, условные типы и utility types

    Дженерики, условные типы и utility types

    В прошлых статьях мы научились описывать форму данных и контрактов с помощью базовых типов, type, interface и классов. Следующий шаг к практической типизации в больших проектах — научиться писать типы, которые подстраиваются под разные данные и умеют вычисляться из других типов.

    Для этого в TypeScript есть три ключевых инструмента:

  • Дженерики (generics) — типы с параметрами, которые можно переиспользовать для разных входных данных.
  • Условные типы (conditional types) — типовая логика вида если тип подходит под условие, то один результат, иначе другой.
  • Utility types — готовые типовые утилиты из стандартной библиотеки TypeScript, которые помогают собирать новые типы из существующих.
  • Официальные материалы:

  • TypeScript Handbook: Generics
  • TypeScript Handbook: Conditional Types
  • TypeScript Handbook: Utility Types
  • !Схема показывает, как дженерики подставляют тип, условные типы выбирают ветку, а utility types собирают новый тип из существующего

    Дженерики

    Зачем нужны дженерики

    Без дженериков часто приходится выбирать одно из двух неудобных решений:

  • потерять типизацию и использовать any или слишком общий тип
  • писать несколько похожих функций/типов для разных типов данных
  • Пример проблемы. Функция возвращает первый элемент массива.

    С дженериком мы сохраняем тип элемента:

    Здесь Tпараметр типа. Он означает: какой бы тип ни был у элементов массива, функция возвращает такой же тип.

    Вывод типа в дженериках

    TypeScript часто может вывести T автоматически:

    Явно задавать T полезно реже, но иногда это нужно, если вывод не срабатывает или вы хотите зафиксировать поведение:

    Несколько параметров типов

    Дженерик может принимать несколько параметров, например, для пары ключ-значение:

    Ограничения (constraints) через extends

    Иногда тип должен быть не любым, а удовлетворять минимальным требованиям. Например, функция работает только со значениями, у которых есть поле length.

    T extends { length: number } читается так: T должен быть чем-то, у чего есть length: number.

    keyof + дженерики: безопасный доступ к полям

    Очень частый практический шаблон: получить значение поля по ключу так, чтобы ключ точно существовал.

    Здесь используется два важных инструмента:

  • keyof T — множество ключей типа T
  • T[K]индексный доступ к типу: тип значения по ключу K
  • Значения по умолчанию для параметров типов

    Можно сделать параметр типа необязательным, задав ему тип по умолчанию.

    Дженерики в интерфейсах и type alias

    Интерфейсы и алиасы типов отлично сочетаются с дженериками.

    Это связывает текущую тему с прошлыми статьями: interface и type остаются основными строительными блоками, а дженерики делают их переиспользуемыми.

    Дженерики в классах

    Классы тоже можно параметризовать типами, особенно когда класс хранит данные.

    Условные типы

    Условные типы позволяют выразить типовую логику:

    Это важно не ради true/false, а чтобы строить разные результирующие типы в зависимости от входного.

    Практический пример: превращаем тип в массив, если это не массив

    Распределяемость (distributive conditional types)

    Если в условном типе слева стоит голый параметр T, то при подстановке union-типа условие применяется к каждому элементу union отдельно.

    Почему так получилось:

  • для string ветка даёт string
  • для number ветка даёт never
  • для boolean ветка даёт never
  • итог объединяется: string | never | never, то есть string
  • Иногда это полезно (как фильтр), а иногда мешает. Если вы хотите не распределять по union, обычно оборачивают в кортеж:

    infer: извлекаем часть типа

    Ключевое слово infer позволяет вытащить тип из другой конструкции.

    Пример: извлечь тип элемента массива.

    Ещё один практический пример: извлечь тип результата Promise.

    TypeScript уже включает похожую утилиту Awaited, но умение писать такие типы полезно, чтобы понимать, как они работают.

    Utility types

    Utility types — это готовые инструменты, которые помогают собирать типы из других типов. Большинство из них основаны на дженериках, keyof, условных типах и infer.

    Самые используемые utility types

    | Утилита | Что делает | Когда полезно | |---|---|---| | Partial<T> | делает все свойства опциональными | патчи, частичное обновление | | Required<T> | делает все свойства обязательными | нормализация данных перед сохранением | | Readonly<T> | запрещает изменять свойства | DTO, конфиги | | Pick<T, K> | выбирает часть полей | публичные проекции типов | | Omit<T, K> | исключает часть полей | скрыть внутренние поля | | Record<K, V> | объект-словарь ключей K со значениями V | индексация, мапы | | Exclude<A, B> | удаляет из union A всё, что входит в B | фильтрация union | | Extract<A, B> | оставляет в union A только то, что входит в B | выделение подмножества | | NonNullable<T> | убирает null и undefined | после валидации | | Parameters<F> | кортеж параметров функции | обёртки над функциями | | ReturnType<F> | возвращаемый тип функции | типизация фабрик | | Awaited<T> | тип значения после await | работа с промисами |

    Partial и патч-обновления

    Сценарий: у нас есть сущность User, и мы хотим обновлять только часть полей.

    Обратите внимание на комбинацию утилит:

  • Omit<User, "id"> исключает id, чтобы его нельзя было менять
  • Partial<...> делает оставшиеся поля опциональными
  • Pick: публичная проекция типа

    Сценарий: вам нужно отдать наружу не всю модель пользователя, а только безопасные поля.

    Record: типизируем словарь

    Сценарий: объект, где ключи — конкретное множество строк, а значения — определённого типа.

    Если вы забудете ключ или добавите лишний, TypeScript подсветит проблему.

    ReturnType и Parameters: типобезопасные обёртки

    Сценарий: вы пишете обёртку над функцией и хотите сохранить сигнатуру.

    Здесь Parameters<F> и ReturnType<F> сохраняют контракт исходной функции.

    Практические рекомендации

  • Сначала моделируйте данные простыми type и interface, а дженерики добавляйте, когда видите реальное повторение.
  • Избегайте дженериков ради “красоты”: слишком сложные типы ухудшают читаемость.
  • Предпочитайте стандартные utility types, если они подходят: это знакомые паттерны для команды.
  • Используйте ограничения (extends), чтобы “непохожий” тип не проходил в функцию по ошибке.
  • Помните про границы рантайма: типы не валидируют данные сами по себе. Если данные приходят извне, типизация должна сочетаться с проверкой (например, на уровне API-слоя).
  • Итог

    В этой статье вы освоили инструменты, которые превращают TypeScript в язык масштабируемой типизации:

  • дженерики для переиспользуемых функций, интерфейсов и классов
  • ограничения extends, связку keyof и T[K] для безопасной работы с ключами
  • условные типы, распределяемость по union и приём с кортежем для отключения распределения
  • infer для извлечения частей типов
  • основные utility types и их практические комбинации (Partial + Omit, Pick, Record, Parameters и ReturnType)
  • Дальше эти знания обычно применяют в типизации реальных границ: API-клиентов, событий, конфигураций, а также при чтении и написании типов для библиотек.

    6. Модули, tsconfig, сборка и интеграция с JavaScript

    Модули, tsconfig, сборка и интеграция с JavaScript

    На предыдущих этапах курса мы учились описывать типы (базовые типы, type и interface), строить API-контракты и применять дженерики и utility types. Но в реальном проекте типизация не живёт в вакууме: код нужно

  • организовать по файлам и модулям
  • настроить правила компиляции в tsconfig.json
  • собрать проект в JavaScript
  • часто смешать TypeScript и JavaScript в одной кодовой базе
  • Эта статья связывает типизацию с инфраструктурой: как TypeScript превращается в рабочий JavaScript, как проект «видит» модули и как безопасно внедрять TS в существующий JS.

    !Схема показывает, что TypeScript проверяет типы и компилирует исходники в JS, а также может генерировать типы и sourcemap

    Что такое модуль в TypeScript

    Модуль — это файл, который имеет собственную область видимости и может экспортировать значения (функции, классы, константы, типы), а также импортировать их из других файлов.

    В TypeScript есть важное разделение:

  • значения существуют в рантайме (например, функция, класс, объект)
  • типы существуют только на этапе компиляции (например, type, interface)
  • Это влияет на импорт/экспорт.

    export и import: базовый синтаксис

    Экспорт значений

    Импорт:

    Экспорт по умолчанию

    Импорт:

    Практическое замечание: default export удобен для одного главного объекта из файла, но именованные экспорты часто проще для рефакторинга и автодополнения.

    Реэкспорт

    Так можно сделать «публичный вход» пакета.

    Импорт и экспорт типов

    Поскольку типы стираются при компиляции, TypeScript предлагает явный синтаксис импорта типов.

    import type полезен тем, что:

  • подчёркивает, что это только тип
  • помогает инструментам сборки точнее убирать лишние импорты
  • TypeScript также умеет export type { User } для реэкспорта именно типов.

    ES Modules и CommonJS: две системы модулей

    В JavaScript исторически есть две основные системы модулей.

  • ES Modules (ESM): import / export
  • CommonJS (CJS): require() / module.exports
  • TypeScript умеет компилировать код под обе системы. Выбор влияет на настройки tsconfig.json и на запуск в Node.js.

    Быстрая таблица различий

    | Тема | ESM | CJS | |---|---|---| | Синтаксис | import/export | require/module.exports | | Node.js | современный стандарт, требует корректных настроек | классический вариант для Node.js | | Совместимость | иногда требует явных расширений и настроек | часто проще с legacy-пакетами |

    Официальные источники:

  • TypeScript Handbook: Modules
  • Node.js Documentation: ECMAScript modules
  • tsconfig.json: что это и зачем

    tsconfig.json — это файл конфигурации TypeScript-проекта. Он определяет:

  • какие файлы входят в проект
  • какие правила типизации включены
  • во что и как компилировать (target, module)
  • куда класть результат (outDir)
  • Минимальный пример:

    include, exclude и files

  • include — какие пути включить (например, src)
  • exclude — что исключить (часто исключают dist, node_modules)
  • files — точечный список файлов (используют реже)
  • Пример:

    Ключевые compilerOptions, которые встречаются в большинстве проектов

    target: в какой JavaScript компилировать

    target задаёт версию JavaScript, в которую компилируется TypeScript.

  • более высокий target сохраняет больше современных возможностей (часто меньше и быстрее код)
  • более низкий target может быть нужен для старых сред
  • Примеры значений: ES2017, ES2020, ES2022.

    module: какую систему модулей генерировать

    module определяет, как будут выглядеть импорты/экспорты в итоговом JavaScript.

    Частые варианты:

  • "module": "CommonJS" для CJS
  • "module": "ES2020" или "module": "NodeNext" для ESM в Node.js
  • Если вы делаете библиотеку, выбор module часто согласуют с тем, как её будут подключать.

    strict: базовый переключатель строгости

    strict: true включает набор строгих проверок (в том числе strictNullChecks, о котором мы говорили в теме базовых типов).

    Практически для обучения и для большинства продакшн-проектов лучше считать strict: true нормой.

    rootDir и outDir: структура входа и выхода

  • rootDir — откуда компилятор «считает» корень исходников
  • outDir — куда складывать скомпилированный JS
  • Пример:

    Типичная структура:

  • src — исходники
  • dist — результат сборки
  • lib: какие встроенные API доступны

    lib определяет, какие стандартные библиотеки доступны типам (например, DOM-API или современные методы массивов).

    Пример для Node.js (без DOM):

    Пример для браузера:

    moduleResolution: как искать модули

    Эта опция отвечает за то, как TypeScript ищет модули по import.

    Для современных Node.js ESM-проектов часто выбирают NodeNext.

    baseUrl и paths: удобные алиасы импортов

    Вместо длинных относительных путей:

    можно настроить алиасы:

    и импортировать так:

    Важно: paths влияет на TypeScript (типизацию и компиляцию), но не всегда автоматически на рантайм. Если вы используете алиасы, убедитесь, что ваша система сборки/запуска тоже их понимает (конкретное решение зависит от инструмента).

    Официальная документация:

  • TypeScript Handbook: tsconfig.json
  • TypeScript Compiler Options
  • Сборка: как компилировать TypeScript в JavaScript

    tsc как основной компилятор

    TypeScript устанавливается как пакет typescript, а команда компиляции обычно выглядит так:

    tsc:

  • читает tsconfig.json
  • проверяет типы
  • генерирует JavaScript в outDir (если он задан)
  • Частые опции сборки

  • sourceMap: true — генерирует .map файлы для отладки (чтобы в девтулз видеть исходный TS)
  • declaration: true — генерирует .d.ts файлы (полезно для библиотек)
  • noEmit: trueне генерировать JS, только проверять типы (часто используют в CI)
  • Пример конфигурации для библиотеки:

    Инкрементальная сборка

    Для ускорения сборки больших проектов можно включить incremental: true. Тогда TypeScript будет хранить информацию о прошлой компиляции и пересобирать только изменённое.

    Интеграция с JavaScript: постепенный переход и смешанные проекты

    TypeScript задуман как постепенно внедряемый. Это означает, что вы можете держать в проекте и .ts, и .js.

    allowJs: включить JavaScript-файлы в проект

    Если у вас уже есть JavaScript-код, можно разрешить его обработку компилятором:

  • allowJs: true — TypeScript будет включать .js файлы
  • checkJs: false — по умолчанию он не будет строго проверять типы в .js
  • checkJs: проверка типов прямо в JavaScript

    Если включить checkJs: true, TypeScript начнёт проверять JavaScript-файлы. Это полезно, если вы хотите улучшать качество кода до миграции на .ts.

    JSDoc-типы в JavaScript

    Даже в .js можно добавлять типы через JSDoc.

    TypeScript сможет использовать эти типы для подсказок и проверок (при checkJs: true).

    Официальная документация:

  • TypeScript Handbook: Type Checking JavaScript Files
  • TypeScript Handbook: JSDoc Reference
  • Как TypeScript «понимает» типы из JavaScript-модулей

    В смешанном проекте у вас есть несколько вариантов:

  • мигрировать файл на .ts и добавить нормальные типы
  • добавить JSDoc
  • создать рядом .d.ts файл с объявлениями типов
  • Пример: есть src/legacy.js:

    Можно добавить декларацию src/legacy.d.ts:

    И тогда TypeScript-код, импортирующий makeUser, получит корректные типы.

    Совместимость импортов: esModuleInterop и default import

    Если вы работаете с CommonJS-библиотеками (особенно в Node.js), часто встречается ситуация, когда хочется писать:

    Для этого во многих проектах включают:

    Смысл: сделать импорт CJS-модулей более удобным в стиле ESM. Эта настройка часто упрощает жизнь, но её лучше включать осознанно и консистентно в проекте.

    Официальная документация:

  • TypeScript Compiler Option: esModuleInterop
  • Практический шаблон конфигурации для небольшого Node.js-проекта

    Ниже пример, который часто подходит для учебных и небольших сервисов.

    Структура

  • src/index.ts
  • dist/index.js
  • tsconfig.json
  • tsconfig.json (вариант под современные Node.js ESM)

    Коротко про skipLibCheck: он отключает проверку типов в декларациях зависимостей (.d.ts внутри node_modules). Это часто ускоряет сборку и снижает шум, но не лечит реальные проблемы в вашем коде.

    Типичные ошибки и как их диагностировать

  • Собралось, но не запускается в Node.js
  • - Проверьте согласованность module и способа запуска (ESM/CJS). - Посмотрите документацию Node.js по ESM и настройку проекта.
  • TypeScript видит алиасы, но рантайм не видит
  • - paths не всегда работает «сам по себе» при запуске. Нужна поддержка со стороны сборщика/раннера.
  • Слишком много ошибок после включения strict
  • - Это нормально при миграции. Двигайтесь постепенно: начните с критичных модулей, используйте unknown вместо any, добавляйте проверки и уточнения типов.

    Итог

    Теперь у вас есть инфраструктурная база, которая связывает все предыдущие темы курса в рабочий проект:

  • вы понимаете, как устроены модули, и как разделяются импорты значений и импорты типов
  • вы умеете настраивать tsconfig.json: target, module, strict, outDir, rootDir, baseUrl/paths
  • вы знаете, как TypeScript компилируется через tsc, и какие опции важны для сборки (sourceMap, declaration, noEmit)
  • вы понимаете стратегии интеграции с JavaScript: allowJs, checkJs, JSDoc и .d.ts
  • Эти навыки нужны, чтобы типы работали не только в примерах, но и в настоящей кодовой базе: с файлами, импортами, сборкой, публикацией и постепенной миграцией.

    7. Работа с библиотеками, типы для API и лучшие практики

    Работа с библиотеками, типы для API и лучшие практики

    До этого мы научились типизировать собственный код: базовые типы, type и interface, классы, дженерики и utility types, а также настроили проект через tsconfig.json. На практике же значительная часть TypeScript-разработки — это работа на границах: взаимодействие с внешними библиотеками и с данными, которые приходят извне (HTTP, файлы, пользовательский ввод). В этой статье разберём, как получать и улучшать типы библиотек, как правильно типизировать API-слой и какие практики реально помогают держать типизацию в порядке.

    !Иллюстрация того, где типизация помогает больше всего — на границах системы

    Как TypeScript “видит” типы библиотек

    TypeScript получает типы для импортируемых модулей из нескольких источников.

  • Встроенные типы в самой библиотеке
  • Типы из сообщества через @types/...
  • Ваши собственные декларации (.d.ts)
  • Встроенные типы (лучший сценарий)

    Многие популярные библиотеки публикуют типы прямо внутри пакета (вместе с .d.ts). Тогда достаточно установить библиотеку:

    И TypeScript начнёт подсказывать типы автоматически.

    Типы из @types (DefinitelyTyped)

    Если библиотека написана на JavaScript и не содержит типы, часто существует пакет с типами в npm под именем @types/....

    Пример:

    Полезные источники:

  • DefinitelyTyped
  • Документация TypeScript: Type Declaration Files
  • Практическое правило: если после установки библиотеки TypeScript пишет, что не может найти файл деклараций для модуля, проверьте наличие @types/<library>.

    Как быстро понять, какие типы использует библиотека

  • В редакторе (например, VS Code) наведитесь на импорт или функцию и откройте Go to Definition.
  • Посмотрите, что именно экспортируется: типы, интерфейсы, перегрузки функций.
  • Обратите внимание на дженерики: библиотеки часто дают возможность “прокинуть” тип результата.
  • Это напрямую связано с прошлой темой про дженерики: большинство “типобезопасных” библиотек строят API вокруг параметров типа.

    Типизация внешних данных: ключевой принцип unknown на границе

    TypeScript не валидирует данные в рантайме. Если вы делаете JSON.parse или получаете JSON по HTTP, в рантайме может приехать что угодно.

    Поэтому хорошая практика:

  • на границе получать unknown
  • проверять
  • только после этого превращать в доменные типы
  • Антипаттерн: “поверили API” через as

    Проблема: as UserDto не проверяет данные, а просто “заставляет” TypeScript молчать.

    Правильнее: парсим unknown и сужаем

    Здесь мы применили технику из темы про объединения и сужение типов: type guard (value is UserDto) превращает unknown в конкретный тип после проверки.

    Типы для API: отделяем DTO от доменной модели

    В проектах удобно различать:

  • DTO-типы (Data Transfer Object): форма данных, как она приходит по сети
  • доменные типы: форма данных, как вы хотите работать с ней в приложении
  • Это важно, потому что внешний API может:

  • менять поля
  • присылать null
  • присылать даты строкой
  • иметь нестабильные форматы
  • Пример: DTO и преобразование

    Так вы удерживаете “грязь внешнего мира” в одном слое, а остальной код работает с удобными типами.

    Типизированный HTTP-клиент: один раз описали — переиспользуем

    Ниже простой паттерн: общий запрос, который возвращает unknown, и функции-обёртки, которые приводят к нужным типам.

    Это хороший пример использования union-типов из базовой темы и аккуратного сужения типов для ошибок.

    Полезные возможности TypeScript при работе с конфигами и константами

    as const: фиксируем литералы и делаем данные неизменяемыми

    Это практичный способ строить union-типы из данных.

    satisfies: проверяем форму, не теряя конкретику

    Оператор satisfies полезен, когда вы хотите:

  • проверить, что объект соответствует контракту
  • но сохранить точные литеральные типы внутри
  • Если забыть ключ или ошибиться в названии, TypeScript покажет ошибку, при этом сам объект не “расширится” до просто Record<string, string>.

    Справка:

  • Документация TypeScript: satisfies operator
  • Когда и как писать свои .d.ts

    Сценарии, когда это нужно:

  • библиотека внутренняя и написана на JS
  • внешняя библиотека не имеет типов и нет @types
  • вам нужно уточнить типы для конкретного случая
  • Минимальный пример декларации модуля

    Если вы импортируете JS-модуль без типов:

    Можно добавить файл legacy-user.d.ts (например, в src/types):

    Это связывает тему модулей и сборки с практикой: компилятор должен видеть .d.ts (проверьте include в tsconfig.json).

    Официально:

  • Документация TypeScript: Declaration Files
  • Расширение типов библиотек: module augmentation

    Иногда нужно добавить поле в тип, который уже объявлен библиотекой (часто в браузере или Node.js).

    Пример: добавим поле в Window.

    Замечания:

  • это обычно делают в отдельном файле, например src/global.d.ts
  • export {} нужен, чтобы файл считался модулем и не загрязнял глобальную область случайно
  • Справка:

  • Документация TypeScript: Declaration Merging
  • Лучшие практики: что реально помогает в продакшне

    Ниже набор практик, которые хорошо сочетаются со всеми темами курса.

    Настройки компилятора

  • включайте strict: true почти всегда
  • добавляйте noEmit: true в отдельный скрипт проверки типов (например, для CI)
  • Справка:

  • Документация TypeScript: tsconfig option strict
  • Документация TypeScript: tsconfig option noEmit
  • Работа с any и unknown

  • избегайте any как “дыры” в типизации
  • используйте unknown на внешних границах и делайте проверки через type guard
  • Типизируйте границы, а не каждую переменную

    Часто достаточно типизировать:

  • публичные функции модуля
  • входные данные (параметры)
  • выходные данные (возврат)
  • структуры DTO
  • Локальные переменные обычно можно оставить на вывод типов.

    Не смешивайте DTO и доменные типы

  • DTO отражает контракт сети
  • доменная модель отражает удобство и правила вашего кода
  • преобразование держите в одном месте
  • Используйте type и interface осознанно

  • interface удобен для публичных объектных контрактов и расширения
  • type удобен для union, intersection, литералов и утилит
  • Это продолжает идеи из темы про type alias и interface.

    Держите типы рядом с кодом, которому они принадлежат

    Обычно проще поддерживать типы, если:

  • типы DTO лежат рядом с API-клиентом
  • доменные типы лежат рядом с бизнес-логикой
  • глобальные расширения (global.d.ts) лежат отдельно и минимальны
  • Итог

    В этой статье мы связали TypeScript-типизацию с реальностью больших проектов:

  • разобрали, откуда берутся типы библиотек: встроенные типы, @types, собственные .d.ts
  • закрепили важный принцип: внешние данные приходят как unknown, и их нужно проверять
  • научились разделять DTO и доменную модель и делать преобразование в одном слое
  • рассмотрели паттерн типизированного API-клиента и вариант с Result<T>
  • применили полезные возможности as const и satisfies
  • обсудили module augmentation и практики поддержки типизации
  • Эта тема завершает картину курса: теперь вы умеете не только писать типизированный код, но и интегрировать его с экосистемой JavaScript, внешними зависимостями и реальными API-контрактами.