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

Senior: питання на співбесіді з теми «Модулі й декларації»

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

4 питання

Triple-slash директиви - коментарі з трьома скісними рисками й XML-тегом, які TypeScript читає як інструкції компілятору:

/// <reference types="vite/client" />
/// <reference path="./legacy-globals.d.ts" />
/// <reference lib="es2025.collection" />

Види:

  • types="..." - підключити декларації пакета (як елемент масиву types у tsconfig, але для конкретного файлу);
  • path="..." - включити інший файл у компіляцію;
  • lib="..." - підключити вбудовану бібліотеку типів (наприклад, нові можливості ECMAScript) без зміни lib у tsconfig.

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

import { a } from './a.js';
/// <reference types="node" />   // ігнорується мовчки

Де вони ще доречні:

  • vite-env.d.ts у проєктах Vite - /// <reference types="vite/client" />: підключає типи import.meta.env і імпорту ресурсів;
  • файли .d.ts, що публікуються в пакеті, - щоб декларації явно залежали від іншого пакета типів (/// <reference types="node" />), коли без глобальних типів Node.js вони не мають сенсу;
  • окремі файли з особливими потребами - тестовий файл, якому потрібні глобальні типи фреймворку, коли решті проєкту вони не потрібні.

Де вони застаріли:

  • path для зв'язку модулів - ES-імпорти роблять це природно; path лишився для старого коду без модулів;
  • /// <reference no-default-lib="true"/> у TypeScript 6 перестала підтримуватися (замість неї - noLib чи libReplacement);
  • /// <amd-module /> втратила сенс разом із видаленням module: amd у TypeScript 6/7.

Порівняно з tsconfig: налаштування в tsconfig.json (types, lib, include) діють на весь проєкт і видні одразу. Директиви «ховають» залежності в окремих файлах. Тому в застосунках перевага за tsconfig, а директиви - для .d.ts і особливих файлів.

Пов'язана зміна TypeScript 6/7: через порожній за замовчуванням types директива /// <reference types="..." /> знову стала корисним способом підключити глобальні типи для окремого файлу.

Докладніше в документації: Triple-slash директиви

Простори імен (namespaces) з'явилися в TypeScript до того, як JavaScript отримав ES-модулі. Вони групують код в іменований об'єкт у глобальній області:

namespace Validation {
  export function isEmail(value: string): boolean {
    return value.includes('@');
  }
}

Validation.isEmail('a@b.ua');

Компілюються в IIFE, що створює об'єкт Validation.

ES-модулі - стандарт JavaScript: файл = модуль, явні import/export, власна область видимості, підтримка браузерами, Node.js і збирачами.

Чому для коду застосунку - модулі, а не namespaces:

  • збирачі розуміють залежності між модулями - tree shaking, розділення коду. Простір імен - один об'єкт, з якого нічого не викинеш;
  • явні залежності видно з імпортів; простори імен покладаються на порядок підключення файлів;
  • стирання типів (Node.js, erasableSyntaxOnly) не підтримує namespaces з виконуваним кодом - їм потрібна генерація;
  • outFile, що склеював простори імен з багатьох файлів в один, видалено в TypeScript 6.

Де namespaces лишилися доречними:

  • у файлах декларацій - для опису бібліотек, що створюють глобальні об'єкти, і для поєднання функції з властивостями (злиття з функцією чи класом);
  • простори імен лише з типами (namespace API { export type User = ... }) - стираються без генерації коду, хоча й тут частіше обходяться модулями;
  • доповнення глобальних просторів: declare global { namespace Express { ... } }, namespace NodeJS.

Ключове слово module для просторів імен: колись простір імен можна було оголосити як module Foo { }. Згодом з'явилося namespace, а module лише не радили. У TypeScript 6 це стало помилкою з можливістю тимчасово приглушити її, у TypeScript 7 - остаточною помилкою:

module Legacy { }      // помилка: використовуйте namespace
namespace Modern { }   // правильно
declare module 'some-lib' { }   // це інше - ambient-оголошення модуля, і воно підтримується

Причина - можлива пропозиція «module blocks» до стандарту JavaScript, з якою старий синтаксис TypeScript конфліктував би.

Міграція старого коду з просторами імен на модулі зазвичай механічна: кожен простір імен - окремий файл з export, звернення Validation.isEmail - імпорт import { isEmail } from './validation.js' або import * as Validation from ....

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

Пакет на TypeScript публікує скомпільований JavaScript і файли декларацій .d.ts (з declaration: true). Як їх знайде споживач - вирішує package.json.

Сучасний варіант - поле exports з умовою types:

{
  "name": "@acme/money",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./format": {
      "types": "./dist/format.d.ts",
      "import": "./dist/format.js"
    }
  },
  "files": ["dist"]
}

Правила, які найчастіше порушують:

  • types - першою умовою в кожному об'єкті. Умови перевіряються по порядку, і TypeScript візьме першу підходящу. Якщо import стоїть раніше, компілятор може «знайти» JavaScript замість декларацій;
  • кожна точка входу в exports має власні types - верхньорівневе поле types для підшляхів не діє;
  • подвійний пакет (ESM + CommonJS) потребує окремих декларацій для кожного формату (index.d.ts і index.d.cts). Одні .d.ts на обидва формати TypeScript інтерпретує в одному режимі - і споживачі з іншим форматом отримують неправильні типи (класична проблема «masquerading as ESM/CJS»);
  • files має включати каталог з .d.ts, інакше їх не буде в опублікованому архіві.

Старе поле types/typings у корені - для споживачів зі старими налаштуваннями резолюції. Опції moduleResolution: node10 у TypeScript 6/7 вже немає, тож сучасні споживачі читають exports; поле в корені можна лишити як запасний варіант.

Залежності типів: якщо публічні декларації посилаються на типи іншого пакета (import type { Request } from 'express'), той пакет (@types/express) має бути в dependencies чи peerDependencies, а не в devDependencies - інакше у споживачів типи зламаються.

Перевірка перед публікацією:

  • @arethetypeswrong/cli - перевіряє, що типи правильно резолвляться для всіх режимів (node16 ESM/CJS, bundler);
  • publint - узгодженість exports, files і реальних файлів;
  • npm pack --dry-run - що саме потрапить в архів.

isolatedDeclarations спрощує генерацію декларацій сторонніми інструментами (і пришвидшує збирання), але вимагає явних типів на експортах.

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

Щоб згенерувати .d.ts для файлу, TypeScript зазвичай виводить типи експортованих функцій і змінних - а для цього потрібен повний аналіз програми з усіма залежностями. Генерувати декларації файл за файлом незалежно неможливо.

isolatedDeclarations (TypeScript 5.5+) вимагає, щоб тип кожного експорту можна було отримати з самого файлу, без виведення через інші модулі. На практиці - явні типи результатів і анотації там, де тип неочевидний:

// помилка з isolatedDeclarations: тип результату треба вивести
export function getTotal(items: Item[]) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ок
export function getTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

export const DEFAULT_PAGE = 1;              // ок: літерал очевидний
export const config = createConfig();       // помилка: потрібна анотація
export const config: AppConfig = createConfig();

Обмеження стосуються лише експортованого API - внутрішній код модуля пишеться як завжди.

Що це дає:

1. Генерація декларацій без перевірки типів. Будь-який інструмент (esbuild, SWC, oxc, Rolldown) може видати .d.ts чисто синтаксично - перетворенням одного файлу, в рази швидше за tsc. Збирання бібліотеки більше не чекає на повну перевірку типів.

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

3. Стабільніший публічний API. Явні типи на експортах означають, що зміна реалізації не змінить мовчки контракт - добра практика для бібліотек незалежно від швидкості.

Ціна: більше анотацій на межах модулів. Редактор пропонує швидке виправлення «додати явний тип», тож міграція переважно механічна.

Кому вмикати:

  • бібліотекам і пакетам монорепозиторію, що публікують .d.ts;
  • великим кодовим базам із проєктними посиланнями.

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

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