Иконка подарка

Весенняя распродажа! Скидка 15% по промокоду

до 01.04.2026

Реэкспорт в index.ts - реэкспорт модулей в TypeScript и JavaScript

27 марта 2026
Автор

Олег Марков

Введение

Реэкспорт в файле index.ts или index.js — это простой прием, который заметно упрощает структуру импорта модулей в проекте. Вместо того чтобы импортировать десятки файлов из разных путей, вы можете собрать их в одном "входном" файле и экспортировать дальше. В итоге другие части кода будут подключать только один модуль — например, папку с этим index.ts — а не каждый файл по отдельности.

Вы, скорее всего, уже встречали конструкции вида:

// barrel-файл index.ts
export * from "./Button"
export * from "./Input"
export * from "./Checkbox"

А потом где‑то в приложении:

import { Button, Input } from "@/components"

Здесь папка components содержит index.ts, который реэкспортирует компоненты. Смотрите, я покажу вам, как это работает и почему такой подход особенно полезен в средних и крупных проектах.

В этой статье мы разберем:

  • что такое реэкспорт и чем он отличается от обычного экспорта
  • какие есть синтаксические варианты реэкспорта
  • как организовывать index.ts в разных слоях приложения
  • типичные ошибки и подводные камни (циклические зависимости, проблемы с типами)
  • практические примеры структуры папок

Цель статьи — чтобы вы могли уверенно использовать index.ts как "точку входа" для модулей, не создавая себе проблем с поддержкой проекта.

Что такое реэкспорт и зачем он нужен

Экспорт, импорт и реэкспорт — важные различия

Для начала давайте уточним терминологию.

  • Экспорт — когда модуль "выставляет наружу" свои сущности:

    • export — именованный экспорт
    • export default — экспорт значения по умолчанию
  • Импорт — когда один модуль использует экспортируемые сущности другого:

    • import { something } from "./module"
  • Реэкспорт — когда модуль импортирует сущность из другого модуля и тут же экспортирует ее дальше. По сути, модуль становится "прокси" между исходным местом и потребителем.

Простейший пример:

// math/add.ts
export function add(a: number, b: number) {
  // Складываем два числа и возвращаем результат
  return a + b
}

// math/index.ts
export { add } from "./add" // Реэкспорт функции add

Теперь другой файл может писать:

import { add } from "./math" // Импорт из index.ts

Вместо:

import { add } from "./math/add"

То есть index.ts здесь — это единая точка доступа ко всем математическим функциям в папке math.

Основные цели реэкспорта в index.ts

Давайте перечислим, какие задачи решает такой подход:

  1. Сокращение количества импорт-путей
    Вам не нужно помнить путь к каждому файлу. Достаточно знать "корневую" папку и то, что в ней есть index.ts.

  2. Сокрытие внутренней структуры папок
    Внешнему коду неважно, как именно вы организовали файлы внутри папки. Хотите — разобьете по подкаталогам, хотите — переименуете файлы. Если index.ts сохраняет тот же интерфейс, внешнему коду ничего менять не нужно.

  3. Создание "публичного API" модуля или пакета
    Вы явно контролируете, что именно можно импортировать извне. В index.ts вы реэкспортируете только то, что считаете "официальным" API.

  4. Удобство рефакторинга
    Когда структура проекта растет, index.ts позволяет перенести реализации в другие файлы, не меняя импорты во всем проекте.

  5. Упрощение автоимпорта в IDE
    Большинство IDE могут подставлять короткий путь до "баррель‑файла" (barrel file) вместо длинных относительных путей.

Синтаксис реэкспорта в TypeScript и JavaScript

Реэкспорт конкретных сущностей

Чаще всего пользуются таким вариантом:

// index.ts
export { Button } from "./Button"
export { Input, InputProps } from "./Input"

Здесь я реэкспортирую только конкретные сущности из указанных модулей. Это более контролируемый способ: вы явно указываете, какие имена выходят наружу.

Важно:

  • Имена после фигурных скобок должны совпадать с именами экспортов в целевых файлах.
  • Можно переименовывать при реэкспорте:
// index.ts
export { Button as PrimaryButton } from "./Button"

Так вы задаете другое имя на уровне публичного API.

Реэкспорт всего содержимого модуля

Иногда проще и быстрее написать:

// index.ts
export * from "./Button"
export * from "./Input"

Это означает: "возьми все экспортируемые сущности из этих файлов и тоже их экспортируй".

Плюсы:

  • Меньше кода
  • Можно быстро собрать все модули в одном месте

Минусы:

  • Меньше контроля над публичным API
  • Вы можете случайно "вынести наружу" то, что не планировалось
  • Возможны конфликты имен (если два модуля экспортируют одно и то же имя)

Если возникает конфликт имен, TypeScript выдаст ошибку компиляции, и вам придется либо переименовать экспорты, либо использовать явное перечисление через export { ... }.

Комбинированный подход

Вы можете комбинировать оба подхода в одном index.ts:

// index.ts
// Явно реэкспортируем важные сущности
export { Button } from "./Button"
export { Input } from "./Input"

// Реэкспортируем все типы из файла с типами
export * from "./types"

Так вы получаете контроль над основным API, но при этом не тратите время на перечисление каждого типа.

Реэкспорт с переименованием

Иногда удобно менять имена на уровне "пакета":

// Button.ts
export function Button() {
  // Реализация компонента кнопки
}

// index.ts
export { Button as PrimaryButton } from "./Button"

Теперь снаружи:

import { PrimaryButton } from "@/ui"

При этом название файла и внутренней функции может не совпадать с именем в публичном API, что дает вам больше свободы при рефакторинге.

Реэкспорт default-экспорта

Default‑экспорт реэкспортируется немного иначе.

Допустим, у вас есть:

// Button.ts
const Button = () => {
  // Реализация компонента
}

export default Button

Вы хотите, чтобы в index.ts этот компонент тоже был доступен как default:

// index.ts
export { default as Button } from "./Button"

Теперь вы можете импортировать:

import { Button } from "@/components"
// или, если index.ts сам делает default-экспорт

Иногда делают и так:

// index.ts
export { default } from "./Button"

Тогда импортируется напрямую default:

import Button from "@/components" // Button — default из index.ts

Но такой подход чаще используется в простых обертках. Если в index.ts много разных сущностей, привычнее использовать именованные экспорты.

Реэкспорт типов в TypeScript

В современных версиях TypeScript вы можете явно указать, что реэкспортируете только типы:

// types.ts
export type User = {
  id: string
  name: string
}

// index.ts
export type { User } from "./types"

Покажу, как это выглядит в использовании:

// other.ts
import type { User } from "@/models"
// Здесь мы используем тип User только на этапе типизации
const user: User = { id: "1", name: "Alex" }

Преимущества такого подхода:

  • TypeScript и бандлер могут лучше "удалять" неиспользуемый код
  • Явно видно, что вы работаете только с типами, а не с реальными значениями

Практические примеры структуры проекта

Пример структуры с компонентами

Давайте разберемся на примере UI‑библиотеки:

src/
  components/
    Button/
      Button.tsx
      Button.styles.ts
      index.ts
    Input/
      Input.tsx
      Input.styles.ts
      index.ts
    index.ts

Содержимое файлов:

// components/Button/Button.tsx
export interface ButtonProps {
  label: string
  onClick: () => void
}

export function Button(props: ButtonProps) {
  // Здесь мы возвращаем JSX с разметкой кнопки
  return <button onClick={props.onClick}>{props.label}</button>
}
// components/Button/index.ts
// Реэкспортируем публичный интерфейс компонента Button
export { Button } from "./Button"
export type { ButtonProps } from "./Button"
// components/Input/Input.tsx
export interface InputProps {
  value: string
  onChange: (value: string) => void
}

export function Input(props: InputProps) {
  // Компонент текстового поля ввода
  return (
    <input
      value={props.value}
      onChange={event => props.onChange(event.target.value)}
    />
  )
}
// components/Input/index.ts
// Собираем публичное API компонента Input
export { Input } from "./Input"
export type { InputProps } from "./Input"
// components/index.ts
// Главный barrel-файл для всех компонентов
export * from "./Button"
export * from "./Input"

Теперь вы можете использовать компоненты так:

// App.tsx
import { Button, Input, type ButtonProps } from "@/components"

// Здесь мы используем Button и Input как часть публичного API папки components

Как видите, вам не нужно помнить про ./components/Button/Button. Достаточно @/components, и IDE подскажет все доступные сущности.

Пример структуры с доменными модулями

Рассмотрим пример бизнес-логики:

src/
  modules/
    user/
      api.ts
      model.ts
      hooks.ts
      index.ts
    product/
      api.ts
      model.ts
      hooks.ts
      index.ts
    index.ts

Содержимое модуля user:

// modules/user/api.ts
export async function fetchUser(id: string) {
  // Отправляем запрос на сервер за данными пользователя
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}
// modules/user/model.ts
export type User = {
  id: string
  name: string
}
// modules/user/hooks.ts
import { useEffect, useState } from "react"
import { fetchUser } from "./api"
import type { User } from "./model"

export function useUser(id: string) {
  // Хук для загрузки и хранения данных пользователя
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    fetchUser(id).then(setUser)
  }, [id])

  return user
}
// modules/user/index.ts
// Публичное API модуля user
export { fetchUser } from "./api"
export { useUser } from "./hooks"
export type { User } from "./model"

Общий index.ts для всех модулей:

// modules/index.ts
// Реэкспортируем все доменные модули
export * from "./user"
export * from "./product"

Теперь в другом месте:

// someFeature.ts
import { useUser, type User } from "@/modules"

// Здесь мы не знаем внутреннюю структуру папки modules
// Мы работаем только с публичным API, собранным в index.ts

Такой подход облегчает рефакторинг: если вы потом решите вынести hooks.ts в другую папку, внешним импортам ничего менять не нужно — вы просто обновите index.ts.

Организация index.ts на разных уровнях проекта

Локальный index.ts в каждой папке

Частая практика — создавать index.ts в каждой "значимой" папке:

  • папка компонента
  • папка фичи
  • папка доменного модуля

Например:

src/
  components/
    Button/
      Button.tsx
      index.ts
    Input/
      Input.tsx
      index.ts
    index.ts
  modules/
    user/
      ...
      index.ts
    index.ts
  index.ts

Выстраивается иерархия "публичных API":

  • src/components/index.ts — API набора компонентов
  • src/modules/index.ts — API доменных модулей
  • src/index.ts — корневой экспортонй файл приложения или библиотеки

Так вы можете:

import { Button } from "@/components"
import { useUser } from "@/modules"

Или, если вы разрабатываете библиотеку, из корневого index.ts экспортировать финальный API:

// src/index.ts
export * from "./components"
export * from "./modules"

Когда index.ts лучше не добавлять

Хотя barrel‑файлы удобны, иногда они могут усложнить структуру. Давайте посмотрим случаи, когда лучше не использовать index.ts:

  1. Маленькая папка с единственным модулем
    Например, если в папке один файл utils.ts, добавлять к нему index.ts ради одного экспорта — излишне.

  2. Сильные циклические зависимости
    Если index.ts импортирует файлы, которые в свою очередь импортируют что‑то из этого же index.ts, можно легко получить циклическую зависимость. В сложных случаях проще отказаться от промежуточного index.ts и импортировать напрямую.

  3. Внутренние утилиты
    Для некоторых "внутренних" модулей может быть полезно, чтобы другие места проекта не могли их импортировать "по случайности". В этом случае не делайте реэкспорт в index.ts, оставляя их как внутренние детали реализации.

Подводные камни и ошибки при реэкспорте

Циклические зависимости

Одна из самых частых проблем — циклические зависимости.

Пример ситуации:

// A/index.ts
export * from "./A"
export * from "../B" // Реэкспортируем все из модуля B

// B/index.ts
export * from "./B"
export * from "../A" // И здесь реэкспортируем все из модуля A

Такая схема приводит к циклу A → B → A. В рантайме в одном из модулей вы можете получить undefined вместо ожидаемой функции или класса.

Как снизить риск:

  1. Избегайте "глобальных" index.ts, которые тянут все подряд
    Лучше делить API по функциональности, а не собирать вообще все в одном файле.

  2. Импортируйте "вглубь", если нужно разорвать цикл
    Вместо импорта из barrel-файла импортируйте конкретный модуль:

    // Плохой вариант — может вызвать цикл
    import { someUtil } from "@/modules"
    
    // Более безопасный вариант
    import { someUtil } from "@/modules/specificModule/utils"
    
  3. Разносите типы и реализации по разным файлам
    Часто циклы возникают, когда сущность и ее типы перемешаны с зависимостями. Вынос типов в отдельный файл и их реэкспорт через export type { ... } помогает избежать проблем.

Конфликты имен при export *

Еще один частый источник ошибок — конфликт имен при использовании export *.

Пример:

// user.ts
export const VERSION = "user-1.0"

// product.ts
export const VERSION = "product-1.0"

// index.ts
export * from "./user"
export * from "./product"

TypeScript не позволит собрать такой файл, потому что имя VERSION определено дважды.

В таких случаях лучше:

  • либо переименовать экспорты в самих модулях
  • либо использовать явный реэкспорт:
// index.ts
export { VERSION as USER_VERSION } from "./user"
export { VERSION as PRODUCT_VERSION } from "./product"

Проблемы с tree shaking при реэкспорте

В современных бандлерах (Webpack, Rollup, Vite) есть поддержка tree shaking — удаления неиспользуемого кода. Реэкспорт может осложнить задачу бандлеру, особенно если:

  • вы используете старый синтаксис require и module.exports
  • смешиваете default‑экспорт и именованные экспорты
  • реэкспортируете целые модули через export * и при этом внутри модуля есть side effects

Рекомендации:

  1. По возможности используйте ESM‑модули (import/export), а не CommonJS.
  2. Для типов — используйте export type и import type, чтобы они не попадали в рантайм.
  3. Старайтесь не класть побочные эффекты (например, изменение глобальных переменных) в модули, которые вы реэкспортируете.

Практические шаблоны использования index.ts

Паттерн "публичное API домена"

Допустим, вы строите решетчатую архитектуру, где каждый домен имеет свое API:

src/
  entities/
    user/
      model.ts
      api.ts
      ui.ts
      index.ts
    order/
      model.ts
      api.ts
      ui.ts
      index.ts
  shared/
    lib/
      index.ts

Внутри домена user:

// entities/user/index.ts
// Публичное API домена user
export type { User } from "./model"
export { fetchUser } from "./api"
export { UserCard } from "./ui"

Где‑то в другой части проекта вы используете:

import { UserCard, fetchUser, type User } from "@/entities/user"

Таким образом, entities/user/index.ts описывает, что можно делать с сущностью User из внешнего мира, а внутренняя организация файлов может меняться.

Паттерн "index.ts как точка сборки типов"

В больших проектах удобно собирать все общие типы:

src/
  types/
    user.ts
    product.ts
    common.ts
    index.ts
// types/user.ts
export type UserId = string
export type UserRole = "admin" | "user"

// types/index.ts
export type { UserId, UserRole } from "./user"
export type { ProductId } from "./product"
export type { ApiResponse } from "./common"

Теперь вы можете писать:

import type { UserId, ApiResponse } from "@/types"

Это выглядит аккуратно и явно показывает, что у вас есть общий слой типов.

Паттерн "переопределение API через index.ts"

Иногда вам нужно скрыть часть интерфейса. Например, есть модуль api.ts, который экспортирует много вспомогательных функций, но вы хотите оставить снаружи только основные.

// api.ts
export async function request(...) {
  // Низкоуровневая функция запроса
}

export function buildUrl(...) {
  // Вспомогательная функция
}

export function logRequest(...) {
  // Логирование
}

export async function getUser(...) {
  // Готовый высокоуровневый запрос за пользователем
}

В index.ts вы можете "очистить" API от лишнего:

// index.ts
// Публичное API для внешних модулей
export { getUser } from "./api"
export type { User } from "./types"

Функции request, buildUrl, logRequest останутся доступны только внутри вашей папки. Такой подход помогает удерживать четкую границу между внутренней и внешней частью модуля.

Заключение

Реэкспорт в index.ts — это не просто синтаксический трюк, а способ выстроить понятное и устойчивое публичное API для модулей вашего проекта. Когда вы выносите все нужные экспорты в один файл, остальные части кода работают только с этим "фасадом", не зависят напрямую от внутренней структуры папок и файлов.

Основные выводы:

  • index.ts удобно использовать как "баррель‑файл" для группировки экспортов.
  • Реэкспорт позволяет сократить количество относительных путей и упростить импорты.
  • Важно осознанно выбирать между export * и явным export { ... }, чтобы не "вынести наружу" лишнее и избежать конфликтов имен.
  • Следует внимательно относиться к циклическим зависимостям и при необходимости импортировать конкретные файлы, обходя barrel‑файлы.
  • Для TypeScript полезно использовать export type и import type, чтобы отделять типы от значений.

Если вы будете рассматривать index.ts как описание публичного контракта модуля, а не просто "свалку реэкспортов", структура проекта станет предсказуемой и удобной в сопровождении.

Частозадаваемые технические вопросы по теме и ответы

Как правильно реэкспортировать смешанные экспорты default и именованные из одного файла

Если в модуле есть и default, и именованные экспорты, можно сделать так:

// module.ts
export default function createModule() { /* ... */ }
export const VERSION = "1.0"

// index.ts
export { default as createModule, VERSION } from "./module"

Теперь вы импортируете:

import { createModule, VERSION } from "@/module"

Такой вариант я рекомендую чаще, чем export { default }, потому что он делает API более явным.

Как реэкспортировать все, кроме одной сущности

Прямого синтаксиса "export * except" нет. Нужно явно перечислить, что вам нужно:

// source.ts
export const a = 1
export const b = 2
export const c = 3

// index.ts
export { a, b } from "./source" // c не реэкспортируем

Если экспортов много, можно сначала импортировать, а затем выбрать:

import * as source from "./source"

export const { a, b } = source

Но обратите внимание, что такой паттерн может немного усложнить анализ для бандлера и типов.

Можно ли использовать index.ts в Node.js с CommonJS

В CommonJS (require/module.exports) реэкспорт делается вручную:

// index.js
module.exports = {
  ...require("./Button"),
  ...require("./Input"),
}

Однако, если есть возможность, лучше использовать ESM‑модули с export/import, даже в Node.js, чтобы сохранить единый стиль и улучшить совместимость с инструментами.

Как ограничить глубину barrel‑файлов чтобы не запутаться

Практика, которую часто используют:

  • Делать index.ts только в "смысловых" папках — компоненты, сущности, фичи.
  • Не собирать "все подряд" в один глобальный index.ts на всю codebase.
  • Если barrel‑файл начинает тянуть слишком много зависимостей, разделить его на несколько более узких: index.ui.ts, index.api.ts и так далее.

Так вы сохраняете удобство импорта, но избегаете чрезмерных связей.

Как лучше настраивать пути импорта при использовании index.ts

Если вы используете TypeScript, можно настроить alias‑пути:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@components/*": ["components/*"],
      "@modules/*": ["modules/*"]
    }
  }
}

Тогда, если в src/components/index.ts собран публичный API компонентов, вы сможете писать:

import { Button } from "@components"

Важно убедиться, что bundler (Vite, Webpack) тоже поддерживает эти alias‑пути, и настроить их аналогично в конфигурации.

Стрелочка влевоПравило относительных импортов в Go - relative-importsПуть к файлу в архитектуре Feature Sliced Design - fsd-pathСтрелочка вправо

Все гайды по Feature-sliced_design

Открыть базу знаний

Отправить комментарий