Middle: питання на співбесіді з теми «Конфігурація й інструменти»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Обидві опції закривають «дірки», які лишає навіть 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».
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 такі помилки виявляться лише під час запуску.
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 перетворить неправильно.
Переписувати все одразу ризиковано й довго. 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- він сам повідомить, коли помилку виправлено і коментар можна прибрати.