Цели
-
Настройка абсолютных импортов без расширений для внутренних модулей пакета с использованием Node.js
subpath imports, как единого источника псевдонимов:без дублирования в tsconfig
path aliases;с поддержкой VSCode IntelliSense - автоимпорт, автодополнение и всплывающие подсказки импортов.
Настройка
conditional exports- маппинг на исходный код внутренней библиотеки в dev-режиме, для работы Vite(HMR) и VSCode(IntelliSense) с живым кодом без пересборки.Настройка приоритета для автоимпорта публичного API модуля (index.ts) через subpath imports.
Пример

Содержание
-
Настройка
conditional subpath exportsвнутренних пакетов/библиотек -
Настройка абсолютных импортов с использованием
conditional subpath imports Приоритет автоимпорта публичного API модуля (index.ts) через subpath imports
Краткий итог
Конфликт между настройками
subpath importsдля Vite и TypeScript решается добавлением customConditions сfallbacks arrayдля TS. Vite читаетdefaultнастройки.-
Автоимпорт (TSServer. Исправлено в tsgo) добавляет
typeв импорт значений, решается:отключением опции VSCode
js/ts.preferences.preferTypeOnlyAutoImportsи заменой
verbatimModuleSyntaxна правила ESLint в dev-режиме.
Автоимпорт публичного API модуля (
index.ts) работает только в extensionless конфигурации.
Вступление
В статье рассмотрены методы решения конфликтов возникающих между инструментами при настройке абсолютных импортов без расширений в режиме ESM.
Изначально весомых аргументов в пользу использования импортов без расширений не было. Планировал проверить техническую возможность вместе с абсолютными импортами. В процессе обнаружил, как получить практическую ценность extensionless конфигурации: настроить приоритет автоимпорта на использование публичного API модуля (index.ts).
Сравнение абсолютных и относительных импортов есть в статьях Максима Земскова и Виталия Потапова.
Не упомянутые настройки: pnpm monorepo, наследуемые конфигурации ESLint и TypeScript, Prettier - можно посмотреть в исходном коде.
Терминология
Спецификатор модуля - это строка с путем к файлу или имя пакета в import/re-export после ключевого слова
fromили аргумент для функции динамического импорта: import().Спецификатор импорта - спецификатор модуля в инструкции import.
Разрешение модуля - это процесс поиска файла, на который ссылается спецификатор модуля. В статье чаще используется
"Разрешение импортов".Абсолютный импорт (в контексте статьи) - это спецификатор модуля (в импорте или реэкспорте) с хэштегом
#настроенный черезsubpath importsи применимый только внутри пакета.TSServer или Language Server в tsgo (реализующий LSP) далее упоминается как TSServer.
Используемые технологии
pnpm monorepo
TypeScript (v7.0.2, v6.0.3, v5.x) с поддержкой формата ESM без расширений
Vite (v8.0) для разработки и сборки web приложения
tsdown для сборки UI библиотеки
-
package.json imports/exports:
conditional subpath imports- настройка псевдонимов для внутренних модулейconditional subpath exports- маппинг на исходники UI библиотеки в dev-режиме
tscдля проверки типовESLint v10 с правилами импорта типов
VSCode - настройки для подсказки и дополнения путей импортов
Состав монорепозитория
apps/web - frontend приложение (React). Сборка: Vite;
packages/ui - компилируемая библиотека (React);
packages/typescript-config - пакет с базовыми конфигурациями TypeScript для наследования;
packages/eslint-config - пакет с базовыми конфигурациями ESLint для наследования.
Особенности инструментов и реализации
В документации и issues инструментов экосистемы часто встречается упоминание реализации алгоритма разрешения модулей Node.js, в т.ч. как “эталонной спецификации”.
Выдержка из документации Node.js:
Когда модуль запрашивается с помощью статических операторов импорта (доступно только в модулях ES) или выражений import() (доступно в модулях CommonJS и ES) алгоритм разрешения модуля:
не поддерживает папки в качестве модуля, индекс каталога (
./api/index.js) должен быть указан полностью;не выполняет поиск расширения. Расширение файла должно быть указано, если спецификатор является относительным или абсолютным URL-адресом файла.
Несмотря на требования Node.js, каждый инструмент по-своему реализует разрешение модулей. Для поддержки спецификаторов с абсолютными путями для внутренних модулей без расширений требуется согласовать настройки.
TypeScript, Turborepo и др. предлагают использовать subpath imports вместо paths aliases для настройки абсолютных импортов внутренних модулей:
(Turbo) Use Node.js subpath imports instead of TypeScript compiler paths
VSCode IntelliSense получает подсказки для импортов через TSServer.
TSServer получает настройки typescript-language-features (VSCode extension) из .vscode/settings.json.
TSServer, tsc, Vite/tsdown (Rolldown) читают:
настройки TypeScript (tsconfig.json),
поля
type,exportsиimportsв package.jsonсобственные файлы конфигурации (vite.config.ts, tsdown.config.ts).
TypeScript (TS) и Vite в режиме разработки (vite dev) обращаются к актуальным исходникам компилируемых библиотек (ui). Для этого у библиотеки в поле exports package.json точки входа по условию "@repo/source" указывают на исходники.
Условия указываются в:
tsconfig.json:compilerOptions.customConditionsvite.config.ts:resolve.conditions.
Tsdown работает в режиме транспиляции кода ui библиотеки из ts в js (опция unbundle: true) и формирует отдельный файл для каждого модуля, что помогает Vite при сборке эффективно резать неиспользуемый код (tree-shaking), а в dev-режиме применять HMR при изменениях в коде библиотеки.
Общие настройки
Для использования формата ESM необходимо указать в пакетах type: module.
package.json:
"name": "@repo/package-name", "type": "module"
Vite
Vite не читает customConditions из tsconfig.json. Чтобы в режиме разработки (vite dev) Vite обращался к актуальным исходникам внутренних компилируемых пакетов (ui) необходимо добавить условие в resolve.conditions:
vite.config.ts
export default defineConfig(({ command }) => { const customConditions: string[] = [] // 'serve': during dev (vite command) if (command === 'serve') { customConditions.push('@repo/source') } return { plugins: [react()], resolve: { conditions: customConditions, }, ...
В режиме сборки (vite build) Vite по умолчанию использует условие default и обращается к скомпилированному коду (см. Настройки компилируемых пакетов).
Полный список используемых Vite условий в зависимости от платформы и типа импорта в документации [Rolldown] (https://rolldown.rs/reference/InputOptions.resolve#conditionnames).
TypeScript
Наследуемые базовые настройки компилятора TypeScript для всех пакетов указаны в packages\typescript-config\base.json
Настройки TypeScript используемые Vite.
Основные настройки TypeScript, влияющие на разрешение модулей:
"compilerOptions": { // Компиляция в формат ESM с поддержкой современных стандартов ECMAScript "module": "esnext", // Указывает TS, что разрешение импортов выполняет бандлер. Не требует расширений. "moduleResolution": "bundler", // Баг TSServer (исправлено в tsgo) с verbatimModuleSyntax: // автоимпорт парных <Tag></Tag> даёт import type вместо import. Работает только <Tag /> или <Tag>. // Аналогичная проблема при использовании настройки VSCode: // `js/ts.preferences.preferTypeOnlyAutoImports: true` "verbatimModuleSyntax": false, // Если outDir не указать, то TS->TSServer->VSCode будет предлагать расширение: .js "outDir": "./dist", "rootDir": "./src", // Учитывать условия в полях exports/imports (package.json) при разрешении путей импортов "customConditions": ["@repo/source", "@repo/ts-subpath-imports"] }
В tsconfig.app.json можно отключить verbatimModuleSyntax - указать false.
verbatimModuleSyntax запрещает импорт/экспорт типов без ключевого слова type.
В dev-режиме валидация осуществляемая verbatimModuleSyntax заменяется на правила ESLint с поддержкой автоисправления:
consistent-type-imports
consistent-type-exports
no-import-type-side-effects
См. packages\eslint-config\typescript-eslint.js.
Для проверки перед сборкой в tsc передается tsconfig.build.json с "verbatimModuleSyntax": true
VSCode IntelliSense
TSServer учитывает настройки VSCode для разрешения импортов. Чтобы TSServer возвращал в VSCode абсолютные пути без расширений в .vscode/settings.json необходимо указать настройки:
"js/ts.preferences.importModuleSpecifier": "non-relative", "js/ts.preferences.importModuleSpecifierEnding": "minimal", // Баг TSServer (исправлено в tsgo) с preferTypeOnlyAutoImports: // автоимпорт парных <Tag></Tag> даёт import type вместо import. Работает только <Tag /> или <Tag>.` // Аналогичная проблема с опцией TypeScript: "verbatimModuleSyntax: true". "js/ts.preferences.preferTypeOnlyAutoImports": false, // В зависимости от style guide. "js/ts.preferences.quoteStyle": "single",
Примечание: опция preferTypeOnlyAutoImports добавлена как альтернатива verbatimModuleSyntax.
Настройка conditional subpath exports внутренних пакетов/библиотек
В терминологии Turborepo внутренние пакеты в зависимости от стратегии компиляции делятся на:
Just-in-Time packages
Compiled packages.
JIT packages компилируются использующим их приложением из исходников.
Compiled packages компилируются сборщиком tsdown, а сборщик приложения (Vite) использует скомпилированный код (в нашем случае - только транспилированный).
Настройки JIT пакетов
Точки входа exports всегда указывают на исходники.
package.json:
"exports": { ".": "./src/index.ts" }
Настройки компилируемых пакетов
Точки входа exports с условиями: “default” и “types” указывают на скомпилированный код и декларации типов.
Условие “@repo/source” указывает на исходники для работы Vite и IntelliSense с актуальным кодом.
Важно добавить “@repo/source” выше default, поскольку TypeScript и бандлеры (Vite, tsdown) по умолчанию включают в условия “default”, а по спецификации Node.js приоритет сопоставления от первого к последнему ключу объекта.
package.json:
"exports": { ".": { "@repo/source": "./src/index.ts", "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }, }
Настройка абсолютных импортов с использованием conditional subpath imports
package.json пакетов web и ui:
"imports": { "#*": { "@repo/ts-subpath-imports": [ "./src/*/index.js", "./src/*.js" ], "default": "./src/*" } },
Индексный файл указывается первым для автоимпорта публичного API модуля.
Почему в целевых путях subpath imports требуются расширения .js: "./src/*/index.js", "./src/*.js"?
TypeScript не выполняет поиск расширения: либо спецификатор должен быть с расширением, либо целевой путь subpath imports.
Если расширение .js указано в subpath imports - TS добавляет к extensionless спецификатору .js и запускает алгоритм поиска файла реализации: File extension substitution
Такой подход позволяет не дублировать в fallbacks array (см. ниже) целевые пути с различными расширениями, как это рекомендуется в документации Turborepo для поддержки импортов без расширений.
Если не указать, то TSServer в автоимпорте и подсказках генерирует спецификаторы импорта с расширениями.
Для чего нужно условие @repo/ts-subpath-imports?
TS понимает массив вариантов (fallbacks array) указанный в полях package.json exports / imports. Vite (oxc-resolver под капотом) придерживается спецификации Node.js и принимает только строку или первый элемент массива для целевых путей subpath imports, issues:
Очевидно было бы добавить строку "./src/*" в начало fallbacks array, но тогда автоимпорты будут с расширением .js.
Поэтому TypeScript будет читать subpath imports через условие @repo/ts-subpath-imports, а Vite - через default.
Особенности работы TSServer при использовании шаблонов (subpath patterns)
Таблица. Особенности шаблонов #*, #src/*, #/*
Шаблон |
Пример |
TS, Node |
Подсказки и автоимпорт |
Дополнение пути |
|---|---|---|---|---|
#* |
import { App } from ‘#app/App’ |
TS: v5.4.0, Node.js: v14.6.0, v12.19.0 |
+ |
с paths |
tsgo: v7.0.2 |
+ |
с paths |
||
#src/* |
import { App } from ‘#src/app/App’ |
TS: v5.7.2, Node.js: v25.4.0, v24.14.0 |
+ |
с paths |
tsgo |
+ |
с paths |
||
#/* |
import { App } from ‘#/app/App’ |
TS: v6.0.2, Node.js: v25.4.0, v24.14.0 |
с paths |
с paths |
tsgo |
+ |
с paths |
Дополнение пути (path completion) - подсказка сегментов пути IntelliSense при ручном вводе работает только с дублированием в paths.
Для шаблона
#/*до tsgo (ts 7.0.2) необходимо указатьcompilerOptions.pathsвweb/tsconfig.app.jsonиui/tsconfig.app.json, иначе возвращается относительный путь:
"compilerOptions": { "paths": { "#/*": ["./src/*"] } }
Приоритет автоимпорта публичного API модуля (index.ts) через subpath imports
Предположим, что модуль предоставляет публичный API через index.ts, чтобы скрыть внутреннюю структуру и детали реализации (например в методологии FSD):
-
src/shared/api/
api.ts - файл, где объявлена переменная api;
index.ts - Публичный API модуля, с реэкспортом переменной api.
Логично, чтобы автоимпорт при генерации спецификатора импорта отдавал приоритет directory module и возвращал:
или папку содержащую файл
index.ts:import { api } from '#shared/api'или полный путь к
index.ts(для настроек с расширениями):import { api } from '#shared/api/index.ts'
Настройка без расширений: необходимо вставить целевой путь с index.js первым в fallbacks array subpath imports - это гарантирует приоритет разрешения модулей:
есть файл
index.ts- автоимпорт указывает на директорию:import { api } from '#shared/api'нет - на файл c объявлением:
import { api } from '#shared/api/api'
Конфигурацию с обязательными расширениями настроить не удалось - автоимпорт отдает приоритет спецификатору импорта, указывающему на файл с объявлением: #shared/api/api.ts.
Дополнение
Не отменяет необходимость архитектурного линтинга (см. Примечания).
-
tsdown и Vite при сборке вырезают barrel (index) файлы для оптимизации. Для этого необходимо пометить их как side-effects free:
Проверка tsc перед сборкой
Перед сборкой необходимо проверить:
соблюдение требований
verbatimModuleSyntax(отключенной в dev-режиме);корректность разрешения модулей с принудительно удаленными
paths.
Общие настройки укажем в наследуемой конфигурации packages/typescript-config/build.json:
"compilerOptions": { "verbatimModuleSyntax": true, // The goal is to stop using path aliases "paths": null }
Пустой tsconfig.build.json в каждом пакете наследует “tsconfig.app.json” и “@repo/typescript-config/build.json”.
Скрипты запуска tsc в package.json пакетов web и ui:
package.json
"typecheck": "tsc -p tsconfig.build.json && tsc -p tsconfig.node.json",
Запуск из корня проекта: pnpm typecheck.
Примечания
Если имя папки импортируемого компонента частично совпадает с именем папки использующего и обе папки находятся на одном уровне, то TSServer игнорирует реэкспорт в barrel file (index.ts) и предлагает путь к tsx, например если есть два компонента:
./src/Iconи./src/IconButton- при импорте Icon из IconButton IDE предлагает путь ‘#Icon/Icon’, вместо ‘#Icon’. Аналогичное поведение при замене имени папкиIconна:I,Ic,Ico,IconB, и т.д. Возможно - issue TypeScript. Временное решение: import-x/no-useless-path-segments-
Циклическая ссылка иногда вешает ESLint - уводит в бесконечную рекурсию: в index.ts файлах нужно использовать относительные пути:
./src/components/Icon/index.ts// export { Icon } from '#components/Icon/Icon' - hangs ESLint. export { Icon } from './Icon.tsx' При смене шаблона подпути (
#*->#/*) в ui/package.json необходимо поправить реэкспорты в корневомindex.ts, иначе автоимпорт возвращает “странные” спецификаторы импорта.
Issues открытые в репозитории TypeScript
Автоимпорт игнорирует index.ts, если имя папки является префиксом имени импортирующего файла (#64029)
Автоимпорт отдаёт приоритет файлам, идущим по алфавиту перед index.ts внутри папки (#64034)
Заключение
Vite (oxc-resolver) при использовании subpath pattern: "#*": "./src/* разрешает модули перебирая расширения и проверяя наличие файла на диске (в т.ч. подставляя индексные файлы: index.ts{x}).
Опции Rolldown/oxc_resolver: extensions, mainFiles.
TypeScript, строго придерживаясь спецификации, не ищет расширения, но предоставляет альтернативу: массив вариантов для проверки (fallbacks array или alternatives в терминологии Webpack)
В issues TypeScript и Vite есть дискуссии (2024г.):
Поскольку алгоритмы работают давно - нет причин ожидать breaking changes без альтернативы.
Vite, tsdown, tsc отлично понимают subpath imports без paths. Paths требуется только для TSServer.
Можно использовать любой из шаблонов subpath imports, с учетом:
#*,#src/*- без дублирования в path;#/*- Доступен с TS 6.0.2, но TSServer IntelliSense требует paths до TS 7 (tsgo);path completionдоступен только с дублированием в paths.
Настройки для всех шаблонов идентичны. Можно добавлять paths при необходимости path completion (отключается перед сборкой в tsconfig.build.json).
Отсутствие дополнения пути в import при ручном вводе не критично (IMHO) при включении в IDE автоимпорта.
TypeScript в процессе перехода на Go. Многие issues связанные с TypeScript language service заморожены, например: Intellisense fails to detect package.json import paths #61504
Подход успешно протестирован на backend (API) приложении: fastify + prisma с транспиляцией tsdown и запуском через tsx --conditions.
Несмотря на предложение рассматривать subpath imports как единый источник правды псевдонимов - инструменты используют его по разному и очень не хватает совместно поддерживаемой документации или руководства по настройке от авторов экосистемы Node.js.
Похожие мысли в статье Марка Эриксона о внедрении поддержки ESM в Redux.
Систематизированный список проблем настройки инструментов в monorepo с поддержкой ESM/TypeScript.
Авторы предлагают добавить новые настройки в package.json для исправления ситуации, например:
"conditions": ["development"], "extensions": [".js", ".jsx", ".ts", ".tsx"]
P.S. Спасибо @Lotgyero за корректуру и наводящие мысли.
Ссылки
Документация
TypeScript. Раздел paths: Both libraries and apps can consider package.json “imports” as a standard replacement for convenience paths aliases.
TypeScript. Разрешение импортов с использованием package.json imports
TypeScript. Почему наименования customConditions должны быть уникальны
Rolldown. Condition names to use when resolving exports in package.json
-
Настройки разрешения модулей:
Turborepo. Раздел TypeScript: Use Node.js subpath imports instead of TypeScript compiler paths
Support seamless cross-package development for monorepos #58626. Ernesto Stifano, Aniello Falcone
Статьи
31 мая 2024. Live types in a TypeScript monorepo. Colin McDonnell
02 сен 2024. Subpath imports с расширениями + vitest. Виталий Потапов
15 окт 2026. Block alternatives with no-restricted-imports. John Reilly
08 авг 2023. My Experience Modernizing Packages to ESM. Mark Erikson
Обновления
27.08.2026: Добавлен блок со ссылками на issues, созданными в репозитории TypeScript