TypeScript Установка и тулчейн TypeScript: от Node до tsconfig
0%

Установка и тулчейн TypeScript: от Node до tsconfig

Установка и тулчейн TypeScript: от Node до tsconfig

Прежде чем писать типы, нужно собрать окружение, которое будет их проверять и превращать .ts в исполняемый код. В отличие от многих языков, у TypeScript нет единого «официального» набора инструментов — есть компилятор tsc и экосистема вокруг него. Разберём каждый слой и то, какие решения принимают в продакшене.

Рантайм и менеджер версий

TypeScript исполняется поверх JavaScript-рантайма. Самый распространённый — Node.js. Устанавливать Node напрямую с сайта неудобно: на разных проектах нужны разные версии. Поэтому в проде используют менеджер версий. Стандарт — nvm или более быстрый fnm.

# установка fnm и последней LTS-версии Node
curl -fsSL https://fnm.vercel.app/install | bash
fnm install --lts
fnm use --lts
node --version   # v22.x — берите актуальную LTS

Зафиксируйте версию Node для проекта в файле .nvmrc (или .node-version), чтобы вся команда и CI использовали одинаковую:

# .nvmrc
22

Альтернативные рантаймы — Deno и Bun — исполняют TypeScript «из коробки», без отдельного шага сборки, и включают тестраннер, форматтер, менеджер пакетов. Они отличный выбор для новых проектов, но экосистема Node пока доминирует в энтерпрайзе, поэтому базовым в курсе будет Node.

Менеджер пакетов и lock-файлы

Пакеты ставятся из реестра npm. Клиентов три основных:

Менеджер Скорость Экономия места Особенности
npm средняя нет идёт в комплекте с Node, «стандарт по умолчанию»
pnpm высокая да (глобальный store + hardlinks) строгий по зависимостям, лучший для монорепо
yarn высокая частично классический yarn устарел, актуален yarn berry (PnP)

Рекомендация для новых проектов — pnpm: он экономит гигабайты за счёт единого глобального хранилища и жёстко изолирует зависимости, не давая «случайно» импортировать пакет, который вы не объявили (проблема phantom dependencies в npm/yarn).

npm install -g pnpm
pnpm init                    # создаёт package.json
pnpm add -D typescript       # devDependency: нужен только на этапе сборки
pnpm add zod                 # runtime-зависимость

Lock-файл (package-lock.json, pnpm-lock.yaml, yarn.lock) фиксирует точные версии всего дерева зависимостей вплоть до хешей. Это гарантирует воспроизводимость сборки: у вас, у коллеги и в CI установятся ровно те же байты.

  • Lock-файл всегда коммитится в git.
  • В CI ставьте зависимости командой, которая падает при расхождении с локом: npm ci, pnpm install --frozen-lockfile, yarn install --immutable. Обычный install может молча обновить лок — в CI это недопустимо.

Разделяйте dependencies (нужны в рантайме) и devDependencies (только для разработки/сборки: typescript, eslint, vitest). В продакшн-образ devDependencies не попадают.

Установка TypeScript и первая проверка

TypeScript ставится локально в проект, а не глобально — так каждый проект фиксирует свою версию в lock-файле.

pnpm add -D typescript
pnpm exec tsc --version      # Version 5.x
pnpm exec tsc --init         # сгенерирует tsconfig.json с комментариями

Сердце проекта: tsconfig.json

tsconfig.json управляет и проверкой типов, и (если используете tsc для сборки) эмитом. Это самый важный конфиг. Разберём осмысленный продакшн-вариант:

{
  "compilerOptions": {
    // --- Целевая версия и модули ---
    "target": "ES2022",           // какой JS-синтаксис на выходе; ES2022 безопасен для Node 18+
    "module": "NodeNext",         // система модулей: NodeNext уважает поле "type" в package.json
    "moduleResolution": "NodeNext", // как резолвить импорты; должен соответствовать module
    "lib": ["ES2022"],            // какие встроенные API доступны (добавьте "DOM" для браузера)

    // --- Строгость: включайте ВСЁ ---
    "strict": true,               // включает 8+ строгих проверок разом (см. ниже)
    "noUncheckedIndexedAccess": true, // arr[i] имеет тип T | undefined — спасает от дыр
    "noImplicitOverride": true,   // требует ключевое слово override
    "exactOptionalPropertyTypes": true, // различает "нет поля" и "поле = undefined"
    "noFallthroughCasesInSwitch": true,

    // --- Совместимость и интероп ---
    "esModuleInterop": true,      // корректный импорт CommonJS-модулей
    "forceConsistentCasingInFileNames": true,
    "verbatimModuleSyntax": true, // явные import type / import — важно для ESM/бандлеров

    // --- Вывод ---
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,          // генерировать .d.ts (для библиотек)
    "declarationMap": true,       // и source maps к ним — «go to definition» ведёт в .ts
    "sourceMap": true,            // маппинг в исходники для отладки и стектрейсов
    "skipLibCheck": true          // не проверять типы внутри node_modules — ускоряет сборку
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Три опции, которые определяют почти всё, и о которых надо думать осознанно:

strict — не обсуждается

strict: true — это переключатель, включающий целое семейство проверок, главная из которых strictNullChecks. Без неё null и undefined входят в любой тип, и вы теряете главную ценность TypeScript — защиту от «миллиардной ошибки» (Cannot read property of undefined). Всегда включайте strict. Для миграции старого кода включайте проверки по одной, но целью всегда должен быть полный strict. Дополнительно всегда включайте noUncheckedIndexedAccess — без неё arr[100] имеет тип элемента, хотя в рантайме там undefined.

target — какой синтаксис на выходе

target определяет, до какого уровня JS понижать синтаксис. ES2022 — разумный современный минимум (поддерживается всеми актуальными Node и браузерами). Слишком низкий target (ES5) заставляет компилятор генерировать многословные полифиллы для классов, async/await, генераторов — раздувает бандл. Ставьте настолько высокий target, насколько позволяет ваша аудитория рантаймов.

module / moduleResolution — ESM против CJS

Это исторически самая мучительная часть. В JS два формата модулей:

  • CommonJS (CJS) — старый Node-формат: require() / module.exports. Синхронный, динамический.
  • ES Modules (ESM) — стандарт языка: import / export. Статический, поддерживает tree-shaking, работает и в браузере, и в Node.

Новые проекты пишут на ESM. Чтобы Node трактовал .js-вывод как ESM, поставьте в package.json поле "type": "module", а в tsconfig — NodeNext. При ESM в Node относительные импорты должны включать расширение:

// ESM в Node: расширение .js обязательно (да, .js, не .ts — импортируется вывод)
import { parseConfig } from "./config.js";

verbatimModuleSyntax: true заставляет писать import type для импортов, которые нужны только на уровне типов, — это устраняет неоднозначности при стирании и критично для корректной работы с бандлерами.

Форматтер и линтер

Разделение обязанностей: Prettier форматирует (отступы, кавычки, переносы), ESLint ищет проблемы (неиспользуемые переменные, опасные паттерны, нарушения правил). Не давайте им конфликтовать: Prettier отвечает за стиль, ESLint — за корректность.

pnpm add -D prettier eslint typescript-eslint

Prettier конфигурируется парой строк — в этом весь смысл, «нет опций — нет споров»:

// .prettierrc.json
{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "printWidth": 100
}

ESLint с новым «flat config» и type-aware правилами (они используют информацию о типах — например, ловят «плавающие» промисы без await):

// eslint.config.mjs
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked, // правила, использующие типы
  {
    languageOptions: {
      parserOptions: {
        projectService: true, // связывает линтер с tsconfig проекта
      },
    },
    rules: {
      "@typescript-eslint/no-floating-promises": "error", // забытый await
      "@typescript-eslint/no-explicit-any": "warn",
    },
  },
);

Скрипты в package.json — единая точка входа для команды и CI:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint .",
    "format": "prettier --write .",
    "build": "tsup src/index.ts --format esm --dts",
    "test": "vitest run"
  }
}

Сборщики: чем эмитить JS

tsc умеет эмитить JS, но медленно и без бандлинга. В проде обычно разделяют проверку типов (tsc --noEmit) и сборку (быстрый инструмент). Ключевые варианты:

Инструмент Что это Когда брать
tsc эталонный компилятор простые библиотеки, где скорость не критична
esbuild сверхбыстрый бандлер на Go максимальная скорость, приложения
tsup обёртка над esbuild библиотеки: одна команда даёт ESM+CJS+.d.ts
Vite dev-сервер + Rollup для прода фронтенд (React/Vue/Svelte)
swc быстрый транспилятор на Rust замена Babel, интеграция с Jest/Next

Главный trade-off: быстрые инструменты (esbuild/swc/tsup) не проверяют типы — они лишь стирают их. Поэтому проверка типов всегда остаётся отдельным шагом:

pnpm typecheck && pnpm build   # сначала гейт типов, потом быстрая сборка

Для библиотеки, публикуемой в npm, tsup — почти идеал: он собирает оба формата модулей и генерирует .d.ts, чтобы потребители получили типы.

Структура проекта и монорепо

Одиночный сервис/библиотека — плоская и предсказуемая раскладка:

my-service/
├── src/
│   ├── index.ts          # точка входа
│   ├── domain/           # бизнес-логика, не знает про фреймворки
│   ├── infra/            # БД, HTTP-клиенты, внешний мир
│   └── config.ts         # разбор и валидация env
├── test/                 # или *.test.ts рядом с кодом
├── tsconfig.json
├── package.json
├── eslint.config.mjs
└── .nvmrc

Когда сервисов и библиотек несколько и они делят код, применяют монорепо. Инструменты: pnpm workspaces для линковки пакетов плюс Turborepo или Nx для кеширования и оркестрации задач. TypeScript связывает пакеты через project references — это ускоряет инкрементальную сборку и обеспечивает корректный порядок.

# pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"

Общее правило раскладки: зависимости направлены внутрь, к домену. Инфраструктура (БД, HTTP) зависит от домена, но не наоборот. Подробно — в файле про архитектуру.

Источники

Что дальше

Окружение готово. Переходим к главному, ради чего мы здесь, — системе типов TypeScript и её фундаментальным механизмам.

Фундамент и система типов

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов