← Курс/tsconfig: продвинутые настройки#195 из 257+20 XP

tsconfig: продвинутые настройки

extends: наследование конфигурации

Можно разделить конфигурацию на базовую и специализированные:

// tsconfig.base.json — общие настройки
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

// tsconfig.json — для разработки
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "sourceMap": true
  }
}

// tsconfig.build.json — для сборки
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "declaration": true,
    "outDir": "./dist"
  },
  "exclude": ["**/*.test.ts", "**/*.spec.ts"]
}

target vs module

Эти два параметра независимы и часто путаются:

{
  "compilerOptions": {
    // target: какой JS выдаёт компилятор
    "target": "ES2020",  // стрелки, async/await, опциональная цепочка

    // module: формат модулей в скомпилированном коде
    "module": "CommonJS",  // require() — для Node.js
    "module": "ESNext",    // import/export — для браузеров/бандлеров
    "module": "Node16",    // для Node.js 16+
  }
}

Типичные комбинации:

  • Node.js: target: ES2022, module: CommonJS или module: Node16
  • React/Vite: target: ES2020, module: ESNext
  • lib: доступные встроенные типы

    {
      "compilerOptions": {
        // Без lib TypeScript использует дефолтные типы по target
        "lib": ["ES2022", "DOM", "DOM.Iterable"]
        // ES2022 — Promise.allSettled, Array.at(), Object.hasOwn()
        // DOM — window, document, HTMLElement
        // DOM.Iterable — NodeList.forEach(), итерация по коллекциям
      }
    }

    Для Node.js без DOM:

    { "lib": ["ES2022"] }

    composite: проектные ссылки

    composite: true включает инкрементальную сборку для монорепо:

    // packages/utils/tsconfig.json
    {
      "compilerOptions": {
        "composite": true,     // обязательно для project references
        "declaration": true,   // генерирует .d.ts
        "declarationMap": true // source maps для .d.ts
      }
    }
    // packages/app/tsconfig.json
    {
      "compilerOptions": { "composite": true },
      "references": [
        { "path": "../utils" },  // зависит от utils
        { "path": "../shared" }
      ]
    }
    # Сборка всех проектов в правильном порядке:
    tsc --build
    tsc -b  # короткая форма
    
    # Только один проект:
    tsc -b packages/app

    incremental: кэширование компиляции

    {
      "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": ".tsbuildinfo"  // кэш-файл
      }
    }

    При повторной компиляции TypeScript перекомпилирует только изменённые файлы.

    Важные отдельные флаги

    {
      "compilerOptions": {
        "skipLibCheck": true,     // не проверять .d.ts файлы зависимостей — ускоряет
        "isolatedModules": true,  // каждый файл независим — требует babel/esbuild транспиляции
        "noEmit": true,           // только проверка типов, без генерации файлов
    
        "resolveJsonModule": true, // import data from './data.json'
        "allowSyntheticDefaultImports": true, // import React from 'react'
        "esModuleInterop": true,  // совместимость CommonJS и ESM импортов
      }
    }

    Полная рекомендуемая конфигурация

    {
      "compilerOptions": {
        "target": "ES2022",
        "lib": ["ES2022", "DOM"],
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitReturns": true,
        "exactOptionalPropertyTypes": true,
        "skipLibCheck": true,
        "incremental": true,
        "declaration": true,
        "declarationMap": true,
        "sourceMap": true
      }
    }

    Примеры

    Симуляция системы tsconfig: наследование конфигов, слияние опций, валидация target/module совместимости

    // Симулируем систему конфигурации TypeScript компилятора.
    // Демонстрируем extends, target vs module, lib, composite.
    
    // --- Встроенные конфиги (как @tsconfig/recommended) ---
    const PRESET_CONFIGS = {
      base: {
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        forceConsistentCasingInFileNames: true,
        declaration: false,
      },
    
      'node-lts': {
        extends: 'base',
        target: 'ES2022',
        lib: ['ES2022'],
        module: 'CommonJS',
        moduleResolution: 'node',
        outDir: './dist',
      },
    
      'node-esm': {
        extends: 'base',
        target: 'ES2022',
        lib: ['ES2022'],
        module: 'Node16',
        moduleResolution: 'Node16',
      },
    
      'react-app': {
        extends: 'base',
        target: 'ES2020',
        lib: ['ES2020', 'DOM', 'DOM.Iterable'],
        module: 'ESNext',
        moduleResolution: 'bundler',
        jsx: 'react-jsx',
        sourceMap: true,
      },
    
      library: {
        extends: 'base',
        target: 'ES2020',
        module: 'ESNext',
        declaration: true,
        declarationMap: true,
        composite: true,
        incremental: true,
      }
    }
    
    // --- Резолвер extends ---
    function resolveConfig(configName, visited = new Set()) {
      if (visited.has(configName)) {
        throw new Error(`Circular extends: ${configName}`)
      }
      visited.add(configName)
    
      const config = PRESET_CONFIGS[configName]
      if (!config) return {}
    
      if (config.extends) {
        const base = resolveConfig(config.extends, visited)
        const { extends: _, ...rest } = config
        // Merge: специфичный конфиг перекрывает базовый
        return mergeDeep(base, rest)
      }
    
      return { ...config }
    }
    
    function mergeDeep(base, override) {
      const result = { ...base }
      for (const [key, val] of Object.entries(override)) {
        if (Array.isArray(val) && Array.isArray(result[key])) {
          result[key] = [...new Set([...result[key], ...val])]
        } else {
          result[key] = val
        }
      }
      return result
    }
    
    // --- Валидация совместимости target/module ---
    const TARGET_FEATURES = {
      'ES5': ['var', 'function', 'prototype'],
      'ES2015': ['arrow', 'class', 'let', 'const', 'Promise', 'Symbol'],
      'ES2017': ['async/await', 'Object.entries', 'Object.values'],
      'ES2020': ['optional-chaining', 'nullish-coalescing', 'BigInt'],
      'ES2022': ['top-level-await', 'class-fields', 'Array.at', 'Object.hasOwn'],
      'ESNext': ['latest-features'],
    }
    
    function getCompatibilityWarnings(config) {
      const warnings = []
    
      // isolatedModules несовместим с const enum
      if (config.isolatedModules && config.module !== 'CommonJS') {
        // OK
      }
    
      // composite требует declaration
      if (config.composite && !config.declaration) {
        warnings.push('composite: true требует declaration: true')
      }
    
      // DOM lib без браузерного target — необычно
      if (config.lib?.includes('DOM') && config.module === 'CommonJS') {
        warnings.push('lib: DOM с module: CommonJS — обычно это Node.js без DOM')
      }
    
      // incremental лучше с tsBuildInfoFile
      if (config.incremental && !config.tsBuildInfoFile) {
        warnings.push('Рекомендуется указать tsBuildInfoFile для incremental')
      }
    
      return warnings
    }
    
    // --- Демонстрация ---
    
    console.log('=== Резолвинг пресетов ===')
    ;['node-lts', 'react-app', 'library'].forEach(preset => {
      const resolved = resolveConfig(preset)
      console.log(`\n[${preset}]`)
      console.log('  target:', resolved.target)
      console.log('  module:', resolved.module)
      console.log('  lib:', resolved.lib?.join(', ') || 'default')
      console.log('  strict:', resolved.strict)
      if (resolved.composite) console.log('  composite:', resolved.composite)
      if (resolved.jsx) console.log('  jsx:', resolved.jsx)
    })
    
    console.log('\n=== Кастомный конфиг с extends ===')
    const customConfig = mergeDeep(
      resolveConfig('react-app'),
      {
        noUnusedLocals: true,
        noUnusedParameters: true,
        noImplicitReturns: true,
        baseUrl: '.',
        paths: { '@/*': ['src/*'] }
      }
    )
    console.log('Resolved custom config:')
    console.log('  strict:', customConfig.strict)
    console.log('  paths:', customConfig.paths)
    console.log('  noUnusedLocals:', customConfig.noUnusedLocals)
    
    console.log('\n=== Проверка совместимости ===')
    const compositeWithoutDeclaration = { composite: true, declaration: false }
    console.log('composite без declaration:', getCompatibilityWarnings(compositeWithoutDeclaration))
    
    const nodeWithDom = { lib: ['ES2022', 'DOM'], module: 'CommonJS', composite: true, declaration: true }
    console.log('Node + DOM:', getCompatibilityWarnings(nodeWithDom))
    
    const incrementalNoFile = { incremental: true }
    console.log('incremental без tsBuildInfoFile:', getCompatibilityWarnings(incrementalNoFile))