Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Конфігурація й інструменти

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

14 питань

tsconfig.json у корені проєкту каже компілятору TypeScript, які файли перевіряти і як це робити. Каталог з цим файлом вважається коренем проєкту.

{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "types": ["vite/client"],
    "paths": { "@/*": ["./resources/js/*"] }
  },
  "include": ["resources/js/**/*.ts", "resources/js/**/*.vue"],
  "exclude": ["node_modules", "public"]
}

Основні розділи:

  • include / exclude / files - які файли входять у проєкт. include приймає шаблони, exclude відкидає частину з них, files - явний список;
  • compilerOptions - як перевіряти й що генерувати: строгість, цільова версія JavaScript, система модулів, шляхи, глобальні типи;
  • extends - успадкувати базовий конфіг (свій спільний або з пакета на кшталт @tsconfig/strictest, @vue/tsconfig);
  • references - посилання на інші проєкти в монорепозиторії.

Важливо розуміти, хто читає tsconfig.json:

  • tsc - повністю;
  • редактор (мовний сервер TypeScript) - для підказок і помилок;
  • збирачі (Vite, esbuild) - лише частину опцій, і не перевіряють типи;
  • Node.js з вбудованим зняттям типів - не читає взагалі.

noEmit: true - типова опція для застосунків на Vite: JavaScript генерує збирач, а tsc лише перевіряє типи.

Нові значення за замовчуванням (TypeScript 6.0+ і 7.0): strict увімкнено, module - esnext, target - останній стабільний стандарт ECMAScript, types - порожній список. Порожній tsconfig.json сьогодні - вже доволі строга конфігурація.

Практична порада: не копіювати чужий конфіг цілком, а починати з базового пресету фреймворку й змінювати лише те, що розумієте, - багато «магічних» опцій зі старих статей у TypeScript 7 уже видалено.

Докладніше в документації: Довідник tsconfig

strict - не одна перевірка, а набір строгих опцій, увімкнених разом:

  • noImplicitAny - помилка, якщо тип не можна вивести і він став би any (параметри функцій без типів);
  • strictNullChecks - null і undefined - окремі типи. string не може бути null, і компілятор змушує перевірити значення перед використанням. Найважливіша з усіх;
  • strictFunctionTypes - коректна (контраваріантна) перевірка параметрів функцій;
  • strictBindCallApply - перевірка аргументів bind, call, apply;
  • strictPropertyInitialization - поля класу мають бути ініціалізовані в конструкторі;
  • noImplicitThis - помилка на this з неявним типом any;
  • useUnknownInCatchVariables - змінна в catch має тип unknown, а не any;
  • alwaysStrict - режим 'use strict' у кожному файлі (у TypeScript 7 його вже не можна вимкнути).
function greet(user: { name: string } | null) {
  return user.name;   // з strictNullChecks: помилка 'user' is possibly 'null'
}

З TypeScript 6.0 strict увімкнено за замовчуванням. Проєкт, що покладався на старе значення false, має явно вказати "strict": false - і це сигнал, що варто планувати перехід.

Чому не вимикати:

  • без strictNullChecks TypeScript мовчить про найпоширеніші помилки виконання - Cannot read properties of undefined;
  • без noImplicitAny значна частина коду фактично не типізована;
  • нові опції строгості з'являються в наборі з новими версіями - проєкт зі strict: true отримує їх автоматично.

Міграція великого проєкту, де одразу тисячі помилок:

  • вмикати опції по одній (strict: false + окремі прапорці), починаючи з noImplicitAny і strictNullChecks;
  • або вмикати strict для нових каталогів через окремі конфіги чи інструменти поступової міграції.

Понад strict корисні noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch - вони в набір strict не входять і вмикаються окремо.

Докладніше в документації: Опція strict

Ці три опції часто плутають, хоча вони відповідають на різні питання.

target - у яку версію JavaScript перетворювати синтаксис.

const user = data?.user ?? guest;

З target: "es2019" компілятор перепише ?. і ?? старим синтаксисом, з es2020 і новіше - лишить як є. target змінює лише синтаксис, а не додає відсутні функції (поліфіли).

У TypeScript 6.0+ значення за замовчуванням - останній стабільний стандарт (зараз es2025), а es5 у TypeScript 7 видалено - найнижча ціль тепер es2015. Для старих браузерів TypeScript-код компілюють іншим інструментом.

lib - які вбудовані API існують у середовищі, для перевірки типів:

"lib": ["es2025", "dom", "dom.iterable"]

Без dom компілятор не знає про document і window; без свіжого es20xx - про Array.prototype.toSorted чи Object.groupBy. Якщо lib не вказано, він береться з target (плюс dom). Для коду в Node.js dom зайвий, натомість потрібен пакет @types/node.

Пастка: lib лише описує типи. Вказати es2025, а запускати код у браузері, який цих методів не має, - помилка виконання, яку TypeScript не впіймає.

module - яку систему модулів генерувати (і як трактувати import/export):

  • esnext / preserve - лишити ES-модулі (типово для коду, який збирає Vite);
  • nodenext - як Node.js: ES-модулі чи CommonJS залежно від "type" у package.json і розширень файлів;
  • commonjs - require / module.exports.

amd, umd, systemjs і none у TypeScript 7 вже не підтримуються.

Пов'язана опція moduleResolution - як знаходити файли за шляхом в import: bundler для Vite, nodenext для коду, що виконує Node.js.

Для типового Vite-застосунку: target і module мало впливають на результат (код генерує збирач), але lib визначає, які API бачить редактор, - його варто узгодити з браузерами, які ви підтримуєте.

Докладніше в документації: Опція target

Замість import Button from '../../../components/Button.vue' зручно писати import Button from '@/components/Button.vue'. Для цього налаштовують псевдоніми шляхів.

У tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./resources/js/*"]
    }
  }
}

Важлива зміна: раніше разом з paths вказували baseUrl. У TypeScript 6.0 його оголосили застарілим, а в TypeScript 7 - видалено (помилка «Option 'baseUrl' has been removed»). Шляхи в paths тепер пишуть відносно каталогу з tsconfig.json, з явним префіксом ./.

Чому однієї опції paths замало: вона лише каже компілятору, де шукати типи. Код вона не змінює - у зібраному JavaScript лишиться import ... from '@/components/Button.vue', і хтось має перетворити цей шлях на справжній.

Тому той самий псевдонім треба налаштувати в інструменті, що виконує чи збирає код:

  • Vite - resolve.alias у vite.config.ts (у Laravel-проєктах @ часто вже налаштовано плагіном чи шаблоном стартового набору):
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  resolve: {
    alias: { '@': fileURLToPath(new URL('./resources/js', import.meta.url)) },
  },
});
  • Vitest - бере resolve.alias з конфігурації Vite;
  • Node.js з вбудованим запуском TypeScript - не читає tsconfig.json і paths не підтримує. Альтернатива - subpath imports у package.json ("imports": { "#/*": "./src/*" }), які розуміють і Node.js, і TypeScript.

Типові помилки:

  • псевдонім є в tsconfig.json, але не у збирачі - редактор задоволений, а збірка падає з «Failed to resolve import»;
  • різні значення в двох місцях - редактор показує один файл, а в зібраному коді опиняється інший;
  • paths у бібліотеці, яку публікують на npm, - у споживачів ці шляхи не працюватимуть; у пакетах краще imports/exports з package.json.

Докладніше в документації: Опція paths

Глобальні змінні середовища - process, Buffer, describe, it, expect - TypeScript знає не сам, а з пакетів типів: @types/node, @types/jest тощо. Опція types визначає, які з таких пакетів підключати глобально, без явного імпорту.

Що змінилося. До TypeScript 5.9 включно значення за замовчуванням було «усі пакети з node_modules/@types». У великих проєктах це сотні пакетів, підтягнутих транзитивно, - повільна перевірка й випадкові глобальні типи.

З TypeScript 6.0 (і в 7.0) types за замовчуванням - порожній список []. Після оновлення проєкт, що покладався на автоматичне підключення, бачить:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node?
Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

Рішення - перелічити потрібні пакети явно:

{
  "compilerOptions": {
    "types": ["node", "vite/client"]
  }
}
  • node - для коду, що працює в Node.js (конфіги, скрипти, SSR);
  • vite/client - типи import.meta.env, import.meta.hot і імпорту ресурсів (.svg, .css) у Vite-застосунку;
  • vitest/globals - якщо тести використовують глобальні describe/it.

Повернути стару поведінку можна значенням ["*"], але документація TypeScript радить явний список: за їхніми даними, багато проєктів пришвидшили збірку на 20-50% лише завдяки цьому.

Що варто знати:

  • types стосується лише глобальних оголошень. Пакети, які ви імпортуєте (import express from 'express'), підключаються через імпорт і не потребують запису в types;
  • різні частини проєкту - різні глобальні типи: код браузера не повинен бачити process, а конфіги Vite - document. Тому шаблони Vite мають два конфіги (tsconfig.app.json і tsconfig.node.json) з різними types і lib;
  • пакет має бути встановлений: запис у types без @types/node у devDependencies дасть помилку «Cannot find type definition file».

Докладніше в документації: Опція types

Обидві опції закривають «дірки», які лишає навіть strict, тому їх часто вмикають додатково.

noUncheckedIndexedAccess - доступ за індексом чи довільним ключем може повернути undefined:

const items: string[] = [];
const first = items[0];        // без опції: string, з опцією: string | undefined
first.toUpperCase();           // з опцією: помилка 'first' is possibly 'undefined'

const prices: Record<string, number> = {};
prices['coffee'].toFixed(2);   // з опцією: помилка

Без опції TypeScript вважає, що елемент масиву чи значення словника завжди є, - і items[0] на порожньому масиві падає під час виконання.

Поведінка, яку варто знати:

  • for...of, map, forEach не потребують перевірок - елемент там точно існує;
  • кортежі з відомою довжиною ([string, number]) не уражені;
  • Map.get() і так повертає T | undefined незалежно від опції.

exactOptionalPropertyTypes - розрізняє «властивості немає» і «властивість є і дорівнює undefined»:

type Options = { theme?: 'dark' | 'light' };

const a: Options = {};                     // гаразд
const b: Options = { theme: undefined };   // з опцією: помилка

Це важливо, бо в JavaScript ці стани поводяться по-різному: 'theme' in options, Object.keys, розгортання { ...defaults, ...options } - явний undefined перезапише значення за замовчуванням. Якщо undefined справді допустиме, його вказують явно: theme?: 'dark' | 'light' | undefined.

Чому вони не в strict:

  • ціна для наявного коду: увімкнення в старому проєкті дає сотні помилок, частина з яких - перевірки там, де розробник «знає», що значення є;
  • шум: arr[i] у звичайному циклі for з індексом теж вимагає перевірки чи !;
  • сумісність з бібліотеками: exactOptionalPropertyTypes виявляє розбіжності в типах сторонніх пакетів.

Рекомендація: у нових проєктах вмикати обидві (пресет @tsconfig/strictest так і робить). У наявних - noUncheckedIndexedAccess у першу чергу: він ловить реальні помилки «undefined is not an object».

Докладніше в документації: Опція noUncheckedIndexedAccess

moduleResolution визначає, як TypeScript шукає файл за рядком в import - і тим самим, чи збігатиметься його уявлення з тим, як код справді виконається.

bundler - для коду, який збирає Vite, webpack, esbuild чи виконує Bun:

import { formatPrice } from './utils';        // без розширення - гаразд
import Button from '@/components/Button.vue';
  • розширення файлів можна не вказувати;
  • підтримує поле exports у package.json залежностей;
  • поєднується з module: "esnext" або "preserve" (з TypeScript 6.0 - і з commonjs).

nodenext - для коду, який напряму виконує Node.js (сервер, CLI, бібліотеки для Node):

import { formatPrice } from './utils.js';   // розширення обов'язкове
  • точно моделює Node.js: ES-модуль чи CommonJS визначається полем "type" у package.json і розширенням (.mts, .cts);
  • розширення обов'язкові у відносних імпортах ES-модулів, як вимагає Node.js;
  • іде в парі з module: "nodenext".

Чому розширення .js, якщо файл .ts: TypeScript не переписує шляхи в імпортах, а після компіляції поруч буде utils.js. Для коду, що виконується через вбудоване зняття типів Node.js, використовують .ts в імпортах разом з опцією rewriteRelativeImportExtensions (для генерації .js) або allowImportingTsExtensions (якщо JavaScript не генерується).

Що обрати:

Код moduleResolution
фронтенд на Vite (Laravel + Vue/React) bundler
сервер чи скрипти, що запускає Node.js nodenext
бібліотека для npm nodenext - так перевірка суворіша й результат працює всюди

Застарілі варіанти: node (він же node10) моделював Node.js 10 і не знав про exports; classic - алгоритм з часів до Node.js. Обидва в TypeScript 7 видалено - помилка «Option 'moduleResolution=node10' has been removed».

Ознака неправильного вибору: TypeScript без помилок перевіряє імпорт, який падає під час виконання (або навпаки). bundler для коду, що запускає Node.js, - типова причина.

Докладніше в документації: Модулі: теорія

Vite, esbuild, oxc, swc і Node.js перетворюють TypeScript на JavaScript по одному файлу і без перевірки типів - просто прибирають анотації. Це швидко, але такий інструмент не бачить інших файлів проєкту. Частина конструкцій TypeScript без знання інших файлів перетворюється неправильно.

Головна проблема - імпорт типу:

// types.ts
export type User = { id: number };

// app.ts
import { User } from './types';

Компілятор TypeScript знає, що User - тип, і прибере імпорт. Інструмент, що бачить лише app.ts, цього не знає: залишить import { User } from './types' - і в браузері помилка «The requested module does not provide an export named 'User'».

isolatedModules - TypeScript попереджає про код, який неможливо безпечно перетворити по одному файлу: реекспорт типу без type, const enum між файлами, файли без імпортів і експортів.

verbatimModuleSyntax - строгіший і простіший підхід: імпорти лишаються в JavaScript рівно так, як написані, крім позначених type. Тому тип треба явно позначити:

import type { User } from './types';
import { fetchUser, type UserFilter } from './api';

З опцією TypeScript видасть помилку на import { User }, якщо User - лише тип.

Що обрати: у сучасних проєктах - verbatimModuleSyntax: true. Він робить поведінку однаковою для tsc, збирачів і Node.js і замінює старіші importsNotUsedAsValues і preserveValueImports. Шаблони Vite вмикають його за замовчуванням.

Наслідки, які варто знати:

  • імпорти з побічними ефектами зберігаються: import './styles.css' нікуди не зникне;
  • import type не виконує модуль - якщо вам потрібен побічний ефект модуля, потрібен звичайний імпорт;
  • з CommonJS опція змушує писати import x = require('...') для CommonJS-виводу - у кодовій базі на ES-модулях це не відчувається;
  • лінтер (@typescript-eslint/consistent-type-imports) автоматично виправляє імпорти, тож перехід на велику кодову базу - один прогін автовиправлення.

Зв'язок з Node.js: вбудоване виконання TypeScript у Node.js теж прибирає лише імпорти з type - без verbatimModuleSyntax такі помилки виявляться лише під час запуску.

Докладніше в документації: Опція verbatimModuleSyntax

Vite лише прибирає типи - перетворює TypeScript на JavaScript без перевірки. Код з помилкою типу const n: number = 'text' спокійно збереться й запуститься. Це навмисне рішення: перевірка типів - повільна операція, що потребує аналізу всього проєкту, а Vite перетворює файли по одному за мілісекунди.

Розподіл обов'язків:

  • редактор (мовний сервер TypeScript, для Vue - розширення Vue - Official) показує помилки під час написання коду;
  • окрема команда перевірки - у збірці й CI.

Типова конфігурація package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit"
  }
}
  • tsc --noEmit - перевірити типи без генерації файлів;
  • для Vue - vue-tsc --noEmit: звичайний tsc не розуміє .vue-файли й не перевіряє шаблони;
  • для проєкту з кількома tsconfig (застосунок і конфіги Node) - tsc -b.

Якщо перевірка типів у build надто сповільнює збірку, її виносять в окремий крок CI, що виконується паралельно зі збиранням.

Помилки типів під час розробки в браузері: плагін vite-plugin-checker запускає перевірку в окремому процесі й показує помилки поверх сторінки.

TypeScript 7 змінює баланс. Нативний компілятор перевіряє великий проєкт у 8-12 разів швидше - перевірка в build і в режимі --watch стає майже непомітною. Але TypeScript 7.0 ще не має програмного API, а інструменти, що його використовують (typescript-eslint підтримує лише TypeScript до 6.x, vue-tsc теж працює через API компілятора), потребують TypeScript 6 - тому в проєкті може знадобитися пакет сумісності @typescript/typescript6 поряд із TypeScript 7.

Наслідки «перевірки лише в редакторі»:

  • помилки в файлах, які ніхто не відкривав, лишаються непоміченими;
  • зміна типу в одному місці ламає десятки інших файлів - редактор покаже це лише у відкритих;
  • тому перевірка в CI обов'язкова, навіть якщо в команді всі користуються редактором з підтримкою TypeScript.

Обмеження, що випливають з покофайлового перетворення, - isolatedModules/verbatimModuleSyntax у tsconfig.json, щоб TypeScript попереджав про конструкції, які Vite перетворить неправильно.

Докладніше в документації: Vite: TypeScript

Переписувати все одразу ризиковано й довго. TypeScript дозволяє змішувати JavaScript і TypeScript в одному проєкті й переходити по файлу.

Крок 1 - tsconfig.json з дозволом JavaScript:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noEmit": true
  },
  "include": ["resources/js"]
}
  • allowJs - .js-файли входять у проєкт: TypeScript-файли можуть їх імпортувати, редактор дає підказки;
  • checkJs - перевіряти й .js-файли. Можна вмикати точково - коментарем // @ts-check на початку окремого файлу.

Крок 2 - типи в JavaScript через JSDoc (без перейменування файлів):

// @ts-check

/**
 * @param {number} amount
 * @param {'UAH' | 'USD'} currency
 * @returns {string}
 */
export function formatPrice(amount, currency) { /* ... */ }

/** @typedef {{ id: number, name: string }} User */

Крок 3 - перейменування файлів у .ts - починаючи з «листових» модулів без залежностей (утиліти, константи, API-клієнт), потім угору до компонентів.

Крок 4 - посилення строгості: спершу noImplicitAny, потім strictNullChecks, наприкінці повний strict.

Що змінилося в TypeScript 7 для JavaScript-файлів. Аналіз JSDoc став ближчим до звичайного TypeScript, частина старих конструкцій більше не розпізнається:

  • @enum не має особливого значення - потрібен @typedef над (typeof Obj)[keyof typeof Obj];
  • синтаксис Closure (function(string): void) замінюється на (s: string) => void;
  • одинокий ? як тип і постфіксний ! не підтримуються;
  • @class не робить функцію конструктором - потрібен class.

Тому JSDoc у старих проєктах після оновлення може дати нові помилки.

Практичні поради:

  • не змінювати поведінку разом з типами - міграція окремими комітами, без рефакторингу логіки;
  • any дозволений як тимчасовий, але з позначкою (// TODO: тип) - і лінтер, що рахує їх кількість;
  • типи для відповідей API - одне з перших, що варто додати: вони дають найбільше користі;
  • @ts-expect-error краще за @ts-ignore - він сам повідомить, коли помилку виправлено і коментар можна прибрати.

Докладніше в документації: Міграція з JavaScript

TypeScript 7 - компілятор, переписаний з TypeScript на Go. Порт робили максимально близько до оригіналу, тож перевірка типів дає ті самі результати, а виграш - у швидкості: нативний код і багатопотоковість.

Що це дає на практиці:

  • повна збірка у 8-12 разів швидша: за даними команди TypeScript, перевірка кодової бази VS Code - 10,6 с замість 125,7 с;
  • редактор: проєкт відкривається й показує першу помилку за секунду-дві замість десятків секунд;
  • пам'ять - зазвичай менше, ніж у TypeScript 6;
  • новий режим --watch на основі файлового спостерігача з Parcel, портованого на Go.

Нові прапорці паралелізму:

  • --checkers N - кількість паралельних перевіряльників типів (за замовчуванням 4). Більше - швидше на потужних машинах, але більше пам'яті; на слабких CI-раннерах варто зменшити;
  • --builders N - паралельна збірка проєктів у --build (монорепозиторії). Множиться з --checkers: 4 × 4 - до 16 перевіряльників одночасно;
  • --singleThreaded - вимкнути паралелізм (налагодження, обмежені середовища).

Різна кількість --checkers у рідкісних випадках може дати різні результати, залежні від порядку, - тому команді варто зафіксувати одне значення для всіх середовищ.

Що потребує уваги при переході:

  • TypeScript 7 бере значення за замовчуванням з 6.0 (strict, types: [], rootDir: ".") і робить помилками все, що 6.0 оголосив застарілим: baseUrl, moduleResolution: node10, target: es5, module: amd/umd, esModuleInterop: false та інше. Рекомендований шлях - спершу перейти на 6.0 і прибрати всі попередження;
  • немає програмного API в 7.0 - його обіцяють у 7.1. Інструменти, що імпортують typescript як бібліотеку (typescript-eslint, частина плагінів збирачів), працюють через пакет сумісності @typescript/typescript6, встановлений поряд через псевдонім npm. Тоді tsc - це TypeScript 7, а інструменти бачать API 6.0;
  • JSDoc у JavaScript-файлах аналізується ближче до .ts - деякі старі шаблони перестали розпізнаватися;
  • шаблонні рядкові типи тепер працюють з кодовими точками Unicode, а не з половинками сурогатних пар.

Що лишилося незмінним: мова, синтаксис і правила перевірки. Код, що компілюється в 6.0 без попереджень, компілюється в 7.0 так само.

Докладніше в документації: Анонс TypeScript 7.0

Сучасний Node.js виконує .ts-файли без окремої збірки: він замінює анотації типів пробілами (type stripping) і запускає те, що лишилося.

node scripts/import-vacancies.ts

Зняття типів увімкнене за замовчуванням з Node.js 22.18 / 23.6 і стабільне з 24.12 / 25.2. Перевірки типів при цьому немає - помилки типів не зупинять запуск.

Обмеження - лише «стиранний» синтаксис. Node.js не генерує код, він тільки прибирає типи. Конструкції TypeScript, що створюють JavaScript, не підтримуються:

  • enum;
  • namespace з кодом під час виконання;
  • параметри-властивості в конструкторі (constructor(private repo: Repo));
  • import x = require(...) і псевдоніми імпорту;
  • декоратори (поки їх немає в самому JavaScript).

Прапорець --experimental-transform-types, що їх перетворював, у Node.js 26 прибрали.

erasableSyntaxOnly (з TypeScript 5.8) змушує tsc повідомляти про такі конструкції заздалегідь:

enum Status { Draft, Published }   // error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled

Замість enum - об'єкт з as const і тип-об'єднання; замість параметрів-властивостей - звичайні поля.

Рекомендовані Node.js налаштування tsconfig.json:

{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

Інші особливості:

  • розширення обов'язкові: import { x } from './utils.ts';
  • import type для типів - інакше Node.js залишить імпорт і впаде (тому verbatimModuleSyntax);
  • tsconfig.json ігнорується: paths не працюють - замість них subpath imports у package.json ("#/*");
  • файли в node_modules не виконуються - пакети мають публікуватися як JavaScript;
  • .tsx не підтримується.

Коли це доречно: скрипти, утиліти, інструменти розробки, невеликі сервіси - там, де збірка була зайвим кроком. Для повної підтримки TypeScript (paths, enum, JSX) - інструменти на кшталт tsx або звичайна збірка.

Докладніше в документації: Node.js: TypeScript

Великий кодовий масив в одному tsconfig.json перевіряється цілком при кожній зміні. Project references ділять його на окремі проєкти з явними залежностями, і TypeScript перевіряє лише змінене та залежне від нього.

Структура:

// tsconfig.json (корінь) - лише посилання
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/web" },
    { "path": "./packages/admin" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

// packages/web/tsconfig.json
{
  "references": [{ "path": "../shared" }]
}
tsc -b            # зібрати всі проєкти в порядку залежностей
tsc -b --watch

composite: true - вимоги до проєкту, на який посилаються: генерація .d.ts, явний rootDir, усі файли в include. Залежний проєкт бачить лише оголошення (.d.ts) залежності, а не її вихідний код - тому перевірка швидша.

incremental - зберегти результати попередньої перевірки у файлі .tsbuildinfo і наступного разу перевіряти лише змінене. Для composite увімкнено автоматично.

Що це дає:

  • швидкість: зміна в admin не змушує перевіряти web;
  • межі: пакет не може імпортувати з іншого, якщо на нього немає посилання, - архітектура перевіряється компілятором;
  • різні налаштування для частин: код браузера з lib: ["dom"], конфіги й сервер - з типами Node.js. Саме так шаблон Vite ділить проєкт на tsconfig.app.json і tsconfig.node.json.

TypeScript 7 збирає незалежні проєкти паралельно (прапорець --builders), тож виграш від поділу ще більший. Обмежувач - граф залежностей: проєкт збирається лише після тих, від яких залежить. Опція isolatedDeclarations дає змогу генерувати .d.ts без перевірки типів залежностей і розпаралелити й це.

Пастки:

  • rootDir з TypeScript 6.0 за замовчуванням - каталог з tsconfig.json. Якщо вихідні файли в src/, а rootDir не вказано, результат опиниться в dist/src/... замість dist/...;
  • застарілі .d.ts: редактор може показувати старі типи залежності, доки її не перезібрано;
  • skipLibCheck (пропустити перевірку .d.ts) теж помітно пришвидшує збірку, але ховає помилки в оголошеннях власних пакетів - його вмикають свідомо.

Докладніше в документації: Project references

TypeScript 6.0 - перехідний реліз: останній на старому компіляторі, який готує проєкти до TypeScript 7. Він змінює значення за замовчуванням і оголошує застарілими опції, які в 7.0 стали помилками.

Нові значення за замовчуванням:

Опція Було Стало
strict false true
module залежно від target esnext
target es5 останній стабільний стандарт (es2025)
types усі @types/* []
rootDir спільний каталог вихідних файлів каталог з tsconfig.json
noUncheckedSideEffectImports false true

Найчастіші проблеми після оновлення:

  • «Cannot find name 'process'» / «describe» - додати "types": ["node", ...];
  • результат збирається в dist/src/index.js замість dist/index.js - вказати "rootDir": "./src";
  • нові помилки строгості - або виправляти, або явно "strict": false як тимчасовий крок.

Видалено в TypeScript 7 (у 6.0 - попередження, які можна приглушити "ignoreDeprecations": "6.0"):

  • baseUrl - шляхи в paths тепер відносно каталогу конфігу з префіксом ./;
  • moduleResolution: node (node10) і classic - замінити на bundler або nodenext;
  • target: es5 і downlevelIteration - найнижча ціль es2015; для ES5 - зовнішній компілятор;
  • module: amd, umd, systemjs, none і outFile;
  • esModuleInterop: false, allowSyntheticDefaultImports: false, alwaysStrict: false;
  • ключове слово module для просторів імен (лише namespace) і asserts в імпортах (лише with);
  • передача файлів у tsc у каталозі з tsconfig.json без --ignoreConfig.

Рекомендований порядок:

  1. оновитися до TypeScript 6.0 і виправити всі попередження про застарілі опції (не приглушувати ignoreDeprecations надовго - у 7.0 це не спрацює);
  2. частину змін (baseUrl, rootDir) робить автоматично експериментальний інструмент ts5to6;
  3. перевірити з прапорцем stableTypeOrdering - у 7.0 він увімкнений і незмінний; з ним 6.0 дає ті самі результати, що й 7.0;
  4. перейти на TypeScript 7, а для інструментів, що потребують API (typescript-eslint), лишити поряд @typescript/typescript6.

Чому ці зміни корисні: явний types пришвидшує перевірку на 20-50%, а видалені опції здебільшого ховали помилки (TypeScript «бачив» імпорти, які не працювали під час виконання).

Докладніше в документації: Анонс TypeScript 6.0