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

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».

Докладніше в документації: Опція 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