TypeScript 7.1 вийшов у бета-версії 20 жовтня 2026 року, і головна фіча релізу – не продуктивність, а точніша типізація import attributes в ambient module declarations. Якщо коротко: тепер можна написати declare module "*" with { type: "css" } { ... } і компілятор нарешті розрізнятиме, який саме атрибут стоїть за імпортом файлу, а не просто дивитиметься на розширення. Встановити бету можна командою npm install -D typescript@beta.

Звучить як дрібниця, але якщо ти хоч раз імпортував CSS, JSON чи бінарні файли через бандлер з явним with { type: ... } – ти знаєш, наскільки криво це працювало раніше.

Чому ambient-декларації були неповноцінними

Import attributes – це метадані після ключового слова with, які кажуть рантайму чи бандлеру, як інтерпретувати імпорт:

import myData from "./data/someData.json" with { type: "json" };

Крім type: "json", екосистема рухається в бік інших атрибутів: ECMAScript-proposal для type: "text" повертає вміст файлу як рядок, proposal для type: "bytes" – як Uint8Array, а браузери вже кілька років підтримують type: "css" для імпорту файлу одразу як об’єкта CSSStyleSheet.

Щоб типізувати такі імпорти, розробники писали wildcard ambient-декларації:

declare module "*.css" {
  const content: CSSStyleSheet;
  export default content;
}

declare module "*.md" {
  const content: string;
  export default content;
}

Проблема в тому, що ця декларація прив’язана лише до розширення файлу і взагалі не бачить сам атрибут with { type: ... }. Звідси три конкретні болі:

  1. Якщо хтось забув дописати with { type: "text" }, TypeScript все одно підставить тип зі старої декларації – помилку ти побачиш тільки в рантаймі.
  2. Доводиться дублювати однакову декларацію для кожного розширення, навіть якщо вони обробляються однаково (.txt і .md обидва повертають type: "text").
  3. Найгірше – бандлер і браузер можуть по-різному інтерпретувати той самий файл залежно від наявності атрибута. Наприклад, бандлер може за замовчуванням застосовувати CSS Modules до .css-файлу, а явний type: "css" у імпорті – перевизначити цю поведінку і повернути сирий CSSStyleSheet. Стара wildcard-декларація просто не могла відрізнити ці два випадки.

Що змінює TypeScript 7.1

Тепер ambient module declarations можна писати з прив’язкою до конкретного import attribute:

declare module "*" with { type: "css" } {
  const content: CSSStyleSheet;
  export default content;
}

declare module "*" with { type: "text" } {
  const content: string;
  export default content;
}

declare module "*" with { type: "bytes" } {
  const content: Uint8Array;
  export default content;
}

Тепер, якщо в проєкті з’явиться import readme from "../README.md"; без атрибута – ти отримаєш помилку компіляції, а не сюрприз під час збірки. А якщо додати with { type: "text" }, TypeScript правильно виведе тип string.

Приклад з практики по типу «до / після»:

// ❌ до 7.1: компілюється, але в рантаймі впаде,
// бо реальний рантайм очікує type-атрибут
import readme from "../README.md";

// ✅ після 7.1: без атрибута – помилка типізації,
// з атрибутом – компілятор знає точний тип
import readme from "../README.md" with { type: "text" };

Чесні обмеження нової фічі

Перш ніж переписувати всі свої .d.ts-файли, варто знати три речі.

По-перше, імпорти, які підпадають під такі декларації, не звіряються з реальними файлами на диску. TypeScript довіряє wildcard-патерну, а не перевіряє, чи файл фізично існує – ця перевірка все одно лягає на рантайм або білд-процес бандлера. Тобто типобезпека тут про форму даних, а не про існування файлу.

По-друге, ці декларації не входять у built-in бібліотеки TypeScript. Команда прямо радить бібліотекам не публікувати такі декларації у своїх npm-пакетах – кінцевий користувач має оголосити їх сам, у своєму проєкті. Причина практична: різні проєкти по-різному налаштовують глобальну область видимості (хтось підключає lib.dom.d.ts, хтось @types/node), і якщо бібліотека нав’яже власну декларацію *.css, це може конфліктувати з тим, що вже є в проєкті.

По-третє, фіча корисна рівно настільки, наскільки твій рантайм/бандлер взагалі підтримує import attributes. Якщо твій стек ще не оперує with { type: ... } – нові ambient-декларації просто не застосовні, і це нормально.

Коли це варто брати

Якщо ти працюєш з інструментами, що вже підтримують import attributes – типізуй кожен type окремою декларацією замість wildcard по розширенню. Це найбільш відчутно для команд, які одночасно тягнуть CSS Modules через бандлер і браузерний type: "css" в одному кодбейсі – саме тут раніше губилася межа між двома різними типами.

Якщо ж твій проєкт імпортує нетипізовані файли рідко і без атрибутів – чекати на фінальний реліз і не поспішати з бетою цілком розумно.

Міні-чекліст перед тим як чіпати свої .d.ts:

  • Чи бандлер або рантайм у проєкті вже підтримує синтаксис with { type: ... }?
  • Чи є у проєкті місця, де один і той самий тип файлу імпортується з різними атрибутами (наприклад CSS Modules за замовчуванням і явний type: "css")?
  • Чи публікуєш ти бібліотеку назовні – і якщо так, чи варто прибрати власні ambient-декларації з пакета, залишивши їх документацією для користувачів?

Окремо варто тримати в полі зору другий анонс з цього релізу – source phase imports, новий ECMAScript-proposal з ключовим словом import source для default-імпортів. Джерело поки описує лише синтаксис (import source SomeModule from "some-module";), а детальну семантику – що саме повертає такий імпорт – команда TypeScript обіцяє розкрити окремо. Поки конкретики мало, і додавати її з голови не варто – почекаємо на повніший опис.

Якщо тобі цікаво, як узагалі влаштований сучасний TypeScript-компілятор і чому команда взялася за нього так активно переписувати, варто глянути TypeScript 7.0: навіщо компілятор переписали на Go і що зміниться у твоєму проєкті – 7.1 логічно продовжує той самий цикл релізів.

Практичний крок на сьогодні простий: постав бету в окремій гілці, перенеси свої wildcard-декларації на варіант з with { type: ... } і подивись, скільки забутих атрибутів компілятор знайде в існуючому коді. Швидше за все – хоч одне місце знайдеться.