Junior: питання на співбесіді з теми «Модулі й декларації»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
TypeScript використовує синтаксис ES-модулів: файл з import чи export на верхньому рівні - модуль зі своєю областю видимості.
// money.ts
export function formatPrice(amount: number): string {
return `${amount.toFixed(2)} грн`;
}
export type Currency = 'UAH' | 'USD';
// app.ts
import { formatPrice, type Currency } from './money.js';
Файл без import/export - скрипт: його оголошення потрапляють у глобальну область. Тому в TypeScript-файлах інколи пишуть порожній export {} - щоб файл став модулем.
Чому .js в імпорті .ts-файлу. TypeScript не переписує шляхи імпортів при компіляції: що написано в коді, те й опиниться в зібраному JavaScript. Після компіляції money.ts стане money.js, і Node.js шукатиме саме ./money.js. Тому з moduleResolution: "nodenext" відносні імпорти пишуть з розширенням виконуваного файлу - .js, а TypeScript розуміє, що йдеться про money.ts.
Що залежить від налаштувань:
moduleResolution: "bundler"(Vite, webpack, esbuild) - розширення можна не писати: збирач сам знайде файл. Найзручніше для фронтенду;moduleResolution: "nodenext"- правила Node.js: розширення обов'язкове для ES-модулів, а тип модуля (ESM чи CommonJS) визначається полем"type"уpackage.jsonабо розширенням.mts/.cts;- запуск
.tsнапряму (стирання типів у Node.js, Bun, Deno) - можна імпортувати з.tsзаallowImportingTsExtensions; для бібліотек, що компілюються,rewriteRelativeImportExtensionsперепише.tsна.jsу результаті.
TypeScript 6/7: старі режими moduleResolution: "node" (node10) і "classic" видалено - лишилися nodenext і bundler. Типове значення module - esnext.
Корисне правило: спосіб резолюції модулів у tsconfig має відповідати тому, хто реально виконує код - збирач, Node.js чи інший рушій. Інакше TypeScript погоджуватиметься з імпортами, які не працюють під час виконання.
import type імпортує лише тип - такий імпорт гарантовано зникає з JavaScript після компіляції.
import type { User } from './models.js';
import { fetchUser, type Role } from './api.js'; // змішаний: fetchUser - значення, Role - тип
export type { User };
Навіщо, якщо TypeScript і так прибирає невикористані в коді імпорти типів:
1. Інструменти, що працюють з одним файлом. Babel, esbuild, SWC, стирання типів у Node.js перетворюють кожен файл окремо, не знаючи, що в іншому файлі User - це інтерфейс, а не клас. Для рядка import { User } from './models.js' вони не можуть вирішити, чи лишати імпорт. import type знімає неоднозначність.
2. Помилки виконання. Якщо імпорт типу лишився в JavaScript, а модуль нічого з такою назвою не експортує (інтерфейси під час виконання не існують), ES-модуль впаде: «The requested module does not provide an export named 'User'».
3. Побічні ефекти й цикли. Звичайний імпорт завантажує модуль і виконує його код. import type - ні. Це розриває циклічні залежності, що існують лише на рівні типів, і не тягне важкі модулі заради одного типу.
4. Читабельність - видно, що з модуля потрібні лише типи.
Прапорець verbatimModuleSyntax робить це обов'язковим: імпорт без type, у якому лише типи, - помилка компіляції. Рекомендований для нових проєктів.
Пастки:
import type { Foo }не можна використати як значення -new Foo()чиFoo.staticMethod()дадуть помилку. Для класу, який використовується і як тип, і як значення, - звичайний імпорт;import type X from(за замовчуванням) іimport type * as ns fromтеж працюють;- різниця
import type { A }іimport { type A }: зverbatimModuleSyntaxперший зникає повністю, а другий лишаєimport {} from './mod.js'- модуль усе одно завантажиться заради побічних ефектів.
Лінтер (@typescript-eslint/consistent-type-imports) автоматично виправляє імпорти на import type.
Файл декларацій .d.ts описує лише типи - без реалізації. Він каже TypeScript, які функції, класи й змінні існують і якого вони типу, а сам код живе деінде (у JavaScript-файлі).
// slugify.d.ts
export default function slugify(text: string, options?: { lower?: boolean }): string;
Звідки беруться декларації:
1. Пакет сам постачає типи. Бібліотека, написана на TypeScript, генерує .d.ts при збиранні (declaration: true) і вказує їх у package.json. Більшість сучасних пакетів так і роблять - нічого встановлювати не треба.
2. Пакети @types/* - для бібліотек, написаних на JavaScript без власних типів. Їх пишуть і підтримують спільнотою в репозиторії DefinitelyTyped:
npm install -D @types/lodash
Версія @types/lodash повторює мажорну й мінорну версію самої бібліотеки - їх варто тримати узгодженими.
3. Вбудовані декларації TypeScript - lib.dom.d.ts, lib.es2025.d.ts тощо. Набір обирається опціями target і lib.
4. Власні декларації в проєкті - для бібліотек без типів, глобальних змінних, файлів-ресурсів (*.svg, *.css).
Як TypeScript знаходить типи імпорту: спершу в самому пакеті (types/exports у package.json), потім у node_modules/@types/назва-пакета.
Важлива зміна в TypeScript 6/7: опція types за замовчуванням тепер порожня ([]). Раніше TypeScript автоматично підключав усі пакети з node_modules/@types як глобальні - через це в кожному проєкті були типи process, describe тощо. Тепер глобальні типи треба перелічити явно:
{ "compilerOptions": { "types": ["node", "vitest/globals"] } }
На типи пакетів, які імпортуються (import _ from 'lodash'), це не впливає - лише на глобальні.
skipLibCheck: true - не перевіряти .d.ts залежностей: значно прискорює збирання, а помилки в чужих деклараціях вам однаково не виправити.
Типовий симптом після оновлення: десятки помилок «Cannot find name 'process'», «Cannot find name 'describe'», «Cannot find module 'fs'».
Причина - нове значення опції types за замовчуванням.
- До TypeScript 6: якщо
typesне вказано, компілятор автоматично підключав усі пакети зnode_modules/@typesяк глобальні декларації. Встановлено@types/node- іprocessдоступний будь-де. - TypeScript 6 і 7:
typesза замовчуванням - порожній масив. Жоден пакет@typesне підключається глобально сам по собі.
Рішення - перелічити потрібні глобальні типи явно:
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
Або для різних частин проєкту - різні tsconfig (тести бачать vitest/globals, а код застосунку - ні).
Повернути стару поведінку можна значенням "types": ["*"], але команда TypeScript радить явний список.
Навіщо це змінили:
- швидкість: у сучасних репозиторіях
node_modules/@typesмістить сотні пакетів, підтягнутих транзитивно. Їх розбір і перевірка займали помітну частину збирання - за даними команди TypeScript, явнийtypesприскорював збирання багатьох проєктів на 20-50%; - передбачуваність: глобальні типи тестового фреймворку не «протікають» у код застосунку (у браузерному коді раптом доступний
describeчиprocess).
На що це НЕ впливає: на типи пакетів, які ви імпортуєте. import express from 'express' знайде @types/express як і раніше. Опція types стосується лише глобальних оголошень.
Інші зміни TypeScript 6/7, що дають схожі «раптові» помилки:
rootDirтепер за замовчуванням - каталог зtsconfig.json. Якщо код уsrc/, а результат раптом з'являється вdist/src/, треба вказати"rootDir": "./src";strict: trueза замовчуванням - проєкти, що покладалися на нестрогий режим, мають явно вказати"strict": false(краще - виправити помилки).
Порада: оновлюватися через TypeScript 6 - він показує попередження про застарілі налаштування, а TypeScript 7 робить їх помилками.
Збирачі (Vite, webpack) дозволяють імпортувати не лише код: import logo from './logo.svg'. TypeScript про такі файли нічого не знає і повідомляє «Cannot find module './logo.svg'».
Рішення - ambient-оголошення модуля з шаблоном:
// src/assets.d.ts
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: Readonly<Record<string, string>>;
export default classes;
}
Тепер будь-який імпорт, що закінчується на .svg, має тип рядка (URL файлу).
Vite вже має такі оголошення - досить підключити їх: "types": ["vite/client"] у tsconfig або /// <reference types="vite/client" /> у файлі vite-env.d.ts. Там описано і ?url, ?raw, ?inline, і import.meta.env.
Чому declare module '*.svg' інколи «не працює». Найчастіша причина - файл з оголошенням є модулем: у ньому є import чи export на верхньому рівні.
// assets.d.ts - НЕ спрацює
import type { FC } from 'react';
declare module '*.svg' {
const Component: FC;
export default Component;
}
У файлі-модулі declare module 'назва' - це доповнення (augmentation) існуючого модуля, а доповнити шаблон, якого немає, неможливо. Ambient-оголошення нових модулів мають бути у скрипті - .d.ts без імпортів на верхньому рівні. Якщо тип потрібен з іншого пакета - імпорт всередині блоку:
declare module '*.svg?react' {
import type { FC, SVGProps } from 'react';
const Component: FC<SVGProps<SVGSVGElement>>;
export default Component;
}
Інші причини:
- файл з оголошеннями не входить у
includeчиfilesуtsconfig; - шаблон не збігається (
'*.svg'проти імпорту з?reactна кінці).
Обережно з «заглушками»: declare module 'some-lib'; без тіла робить усі імпорти з пакета типу any - це прибирає помилку, але й будь-яку перевірку. Для бібліотек краще знайти чи написати справжні типи.