transpileOnly и отдельный запуск tsc для проверки типов

При работе с TypeScript в связке с Webpack часто возникает проблема: полноценная проверка типов существенно замедляет сборку. Особенно это заметно в крупных проектах, где тысячи .ts и .tsx файлов проходят через ts-loader.

Для ускорения сборки используется режим transpileOnly. В этом режиме TypeScript-компилятор выполняет только транспиляцию кода:

  • преобразует TypeScript в JavaScript;
  • удаляет типы;
  • обрабатывает импорты и синтаксис;
  • генерирует итоговый JS-код.

При этом полностью отключается:

  • semantic type checking;
  • анализ совместимости типов;
  • проверка ошибок интерфейсов;
  • контроль generic-ограничений;
  • большинство диагностик TypeScript.

Главная идея заключается в разделении задач:

Процесс Ответственность
Webpack + ts-loader Быстрая сборка
tsc --noEmit Полная проверка типов

Такой подход стал стандартом для крупных frontend-проектов.


Как работает обычный ts-loader

Без transpileOnly loader вызывает полноценный TypeScript Compiler API:

{
  test: /\.ts$/,
  use: 'ts-loader'
}

Внутри происходит:

  1. Построение dependency graph.
  2. Анализ типов.
  3. Проверка деклараций.
  4. Инференс generic-типов.
  5. Проверка импортов.
  6. Эмиссия JavaScript.

Фактически TypeScript выполняет почти ту же работу, что и команда:

tsc

В результате:

  • возрастает потребление памяти;
  • увеличивается время hot reload;
  • замедляется incremental build;
  • ухудшается скорость dev-server.

Принцип работы transpileOnly

Опция включает облегчённый режим работы loader-а:

{
  loader: 'ts-loader',
  options: {
    transpileOnly: true
  }
}

Теперь TypeScript выполняет только:

TypeScript -> JavaScript

Без анализа типов.

Это означает:

  • сборка становится значительно быстрее;
  • Webpack перестаёт ждать type checker;
  • HMR работает быстрее;
  • уменьшается нагрузка на CPU.

Типичные ускорения сборки

В реальных проектах ускорение обычно выглядит так:

Размер проекта Обычная сборка transpileOnly
Малый 3 сек 2 сек
Средний 15 сек 6 сек
Крупный 60+ сек 15–20 сек

Наибольший эффект наблюдается:

  • при большом количестве generic-типов;
  • при использовании React + TSX;
  • при активном HMR;
  • при monorepo-архитектуре;
  • при наличии большого числа declaration files.

Базовая конфигурация

Установка зависимостей

npm install webpack webpack-cli typescript ts-loader --save-dev

webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',

  entry: './src/index.ts',

  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },

  resolve: {
    extensions: ['.ts', '.js']
  },

  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      }
    ]
  }
};

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "sourceMap": true
  }
}

Что отключается при transpileOnly

После включения режима TypeScript перестаёт сообщать об ошибках типов.

Пример:

const age: number = '25';

При обычной сборке:

Type 'string' is not assignable to type 'number'

При transpileOnly:

  • Webpack успешно соберёт проект;
  • ошибка не появится;
  • некорректный код попадёт в bundle.

Это ключевая особенность режима.


Почему отдельный запуск tsc обязателен

Без проверки типов проект становится потенциально нестабильным.

Поэтому применяется отдельная команда:

tsc --noEmit

Она выполняет:

  • полную проверку типов;
  • анализ проекта;
  • validation импортов;
  • проверку declaration merging;
  • анализ generics;
  • контроль strict-mode.

Но при этом не создаёт JS-файлы.

Опция:

--noEmit

отключает генерацию выходных файлов.


Разделение обязанностей

Современная архитектура TypeScript-проекта обычно выглядит так:

Инструмент Задача
Webpack Сборка
ts-loader transpileOnly Трансформация TS → JS
tsc –noEmit Проверка типов
ESLint Линтинг
Babel/SWC/esbuild Дополнительная трансформация

Такой подход обеспечивает:

  • высокую скорость разработки;
  • сохранение строгой типизации;
  • масштабируемость;
  • быстрый HMR.

Проверка типов через npm scripts

package.json

{
  "scripts": {
    "build": "webpack",
    "typecheck": "tsc --noEmit",
    "dev": "webpack serve"
  }
}

Теперь:

npm run dev

используется для быстрой разработки.

А:

npm run typecheck

для полной валидации типов.


Проверка типов в CI/CD

Обычно pipeline строится так:

npm run typecheck
npm run build

Либо:

tsc --noEmit && webpack

Это гарантирует:

  • отсутствие type errors;
  • сохранение быстрого Webpack;
  • корректную production-сборку.

Использование fork-ts-checker-webpack-plugin

Отдельный запуск tsc — не единственный вариант.

Существует плагин:

npm install fork-ts-checker-webpack-plugin --save-dev

Он переносит проверку типов в отдельный процесс.


Конфигурация

const ForkTsCheckerWebpackPlugin = require(
  'fork-ts-checker-webpack-plugin'
);

module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: {
          transpileOnly: true
        }
      }
    ]
  },

  plugins: [
    new ForkTsCheckerWebpackPlugin()
  ]
};

Как работает ForkTsChecker

Архитектура становится следующей:

Webpack Thread
 └─ transpilation only

Separate Worker Process
 └─ type checking

Преимущества:

  • сборка не блокируется;
  • проверка типов выполняется параллельно;
  • UI dev-server остаётся быстрым;
  • ошибки всё равно отображаются в консоли.

Сравнение подходов

Подход Скорость Проверка типов Сложность
ts-loader обычный Низкая Да Низкая
transpileOnly + tsc Высокая Да Средняя
transpileOnly + ForkTsChecker Очень высокая Да Средняя

Почему transpileOnly особенно важен в больших проектах

TypeScript type checker плохо масштабируется при:

  • глубокой вложенности generic-типов;
  • union types большого размера;
  • conditional types;
  • mapped types;
  • сложных React props;
  • большом числе declaration files.

В некоторых enterprise-проектах именно type checker становится главным bottleneck сборки.

Трансформация кода при этом занимает значительно меньше времени.


Пример проблемы производительности

type DeepPartial<T> = {
  [P in keyof T]?: DeepPartial<T[P]>;
};

При огромных объектах TypeScript может тратить значительное время на recursive type analysis.

transpileOnly полностью исключает эти вычисления из Webpack pipeline.


Особенности работы source maps

Даже при отключённой проверке типов source maps продолжают работать:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

Debugging остаётся полноценным:

  • breakpoint-ы;
  • stack trace;
  • mapping TS → JS;
  • devtools integration.

Совместимость с Babel

Очень распространённая архитектура:

ts-loader (transpileOnly)
        ↓
babel-loader
        ↓
webpack

Либо:

Babel TypeScript preset
        ↓
Webpack

Причина в том, что Babel вообще не умеет проверять типы.

Поэтому отдельный запуск:

tsc --noEmit

становится обязательным.


Пример Babel-конфигурации

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          {
            loader: 'babel-loader'
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true
            }
          }
        ]
      }
    ]
  }
};

Использование только Babel

Иногда TypeScript используется исключительно как syntax layer:

{
  test: /\.ts$/,
  loader: 'babel-loader'
}

С preset:

@babel/preset-typescript

Но тогда:

  • типы полностью игнорируются во время сборки;
  • ошибки обнаруживаются только через tsc.

Инкрементальная проверка типов

Для ускорения tsc можно включить:

{
  "compilerOptions": {
    "incremental": true
  }
}

TypeScript создаст:

.tsbuildinfo

Повторные проверки станут значительно быстрее.


Использование composite-проектов

В monorepo часто применяется:

{
  "compilerOptions": {
    "composite": true
  }
}

И references:

{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" }
  ]
}

Это позволяет:

  • кэшировать результаты проверки;
  • ускорять typecheck;
  • уменьшать время CI.

Проблемы, которые может скрыть transpileOnly

Некорректные импорты

import { test } from './missing';

Webpack может не обнаружить часть ошибок типов до runtime.


Несовместимые generic-типы

function log<T extends number>(value: T) {}

log('hello');

Ошибка пропадёт во время сборки.


Ошибки React props

<Button size={123} />

Bundle будет создан даже при несовместимых типах.


Влияние на IDE

Даже если Webpack работает без проверки типов:

  • VSCode;
  • PhpStorm;
  • WebStorm;

продолжают использовать TypeScript Language Server.

Поэтому:

  • ошибки подсвечиваются в редакторе;
  • autocomplete работает;
  • inference сохраняется.

Это одна из причин популярности transpileOnly.


Использование с thread-loader

Дополнительное ускорение:

npm install thread-loader --save-dev

Конфигурация

{
  test: /\.ts$/,
  use: [
    'thread-loader',
    {
      loader: 'ts-loader',
      options: {
        transpileOnly: true,
        happyPackMode: true
      }
    }
  ]
}

Что делает happyPackMode

Режим:

happyPackMode: true

отключает некоторые внутренние механизмы синхронизации TypeScript-loader-а.

Используется:

  • вместе с thread-loader;
  • при parallel compilation;
  • для максимального ускорения.

Ограничения happyPackMode

Возможны проблемы:

  • с custom transformers;
  • с некоторыми plugin hooks;
  • с нестандартными resolver-ами.

В обычных проектах режим работает стабильно.


Когда transpileOnly использовать не стоит

Режим может быть нежелателен:

  • в небольших проектах;
  • в библиотеках со сложной публичной типизацией;
  • при разработке declaration-файлов;
  • при debugging сложных type-level конструкций.

В маленьких проектах выигрыш в скорости может быть минимальным.


Типичная production-схема

Development

webpack-dev-server
 + transpileOnly
 + ForkTsChecker

CI

tsc --noEmit

Production Build

webpack --mode production

Почему схема стала индустриальным стандартом

Причины популярности:

  1. Type checking — самая дорогая часть TypeScript.
  2. Трансформация TS → JS относительно дешёвая.
  3. Проверку типов можно вынести отдельно.
  4. Webpack должен собирать максимально быстро.
  5. HMR критически зависит от скорости rebuild.

В результате почти все крупные frontend-проекты используют одну из схем:

  • transpileOnly + tsc;
  • transpileOnly + ForkTsChecker;
  • Babel/SWC/esbuild + отдельный typecheck.

Сравнение с esbuild и swc

Современные транспайлеры:

  • esbuild;
  • SWC;

работают по тому же принципу:

Types are erased
without semantic checking

То есть:

  • выполняется только transpilation;
  • типы удаляются;
  • semantic analysis отсутствует.

Поэтому отдельный:

tsc --noEmit

остаётся обязательной частью production pipeline даже при отказе от ts-loader.