$ sudo teach IT
МОДУЛЬ 9 · УРОК 2

Default export

export default, import без фигурных скобок, когда использовать

📖 Введение

Представьте, что вы заходите в библиотеку. В каталоге написано: «Книга: "Война и мир" — Толстой Л.Н.». Вы знаете, что ищете именно эту книгу. Но что если бы в каталоге было написано: «Имена: Толстой Л.Н., Пушкин А.С., Достоевский Ф.М., Чехов А.П.»? Вы бы должны были выбрать нужное имя из списка.

Именно так работают именованные экспорты — вы перечисляете конкретные имена. Но что если модуль экспортирует только одну основную вещь? Тогда зачем заставлять пользователя каждый раз писать имя в фигурных скобках?

Default export (экспорт по умолчанию) — это специальный тип экспорта, который позволяет импортировать модуль без указания имени в фигурных скобках. Это удобно, когда модуль экспортирует одну главную сущность — класс, функцию или объект.

Давайте разберём подробнее, как работает default export, чем он отличается от именованного, и когда его стоит использовать.

1 Named export vs Default export — в чём разница

Давайте сравним два типа экспорта на простом примере:

// Named export
export const VERSION = '1.0.0';
export function greet(name: string): string {
  return `Привет, ${name}!`;
}

// Импорт:
import { VERSION, greet } from './utils';
// Default export
export default function greet(name: string): string {
  return `Привет, ${name}!`;
}

// Импорт:
import greet from './utils';

Ключевые отличия:

  • Default export не требует имени в фигурных скобках при импорте
  • Default export можно импортировать под любым именем
  • В модуле может быть только один default export
  • Default export можно комбинировать с именованными экспортами
  • Named export — более строгий: имя при импорте должно совпадать с экспортом

Аналогия: Именованный экспорт — это меню в ресторане: вы выбираете конкретное блюдо. Default экспорт — это «комплексный обед»: вам дают набор по умолчанию, и вы его просто берёте без выбора.

Ещё одна аналогия: именованный экспорт — это полка в магазине с подписанными продуктами. Вы берёте именно то, что нужно. Default экспорт — это коробка-сюрприз: внутри одна главная вещь, и вы её просто забираете.

2 Синтаксис default export — три способа

Есть три способа создать default export:

1. Экспорт сразу при объявлении:

// Экспорт функции
export default function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

// Экспорт класса
export default class User {
  constructor(public name: string, public email: string) {}
}

// Экспорт константы
export default const APP_NAME = 'My App';

// Экспорт объекта
export default {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

2. Отдельный export default в конце файла:

function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

// Экспорт уже объявленной функции
export default calculateTotal;

// Или класса:
class UserService {
  // ...
}

export default UserService;

3. Анонимный default export:

export default function(price: number, quantity: number): number {
  return price * quantity;
}

Третий способ используется редко, потому что функция не имеет имени — это затрудняет отладку и чтение стек-трейсов. Когда вы видите ошибку в консоли, анонимная функция будет отображаться как <anonymous>, что не помогает понять, где именно произошла ошибка.

Совет: Всегда давайте имена default export-ам. Это упрощает чтение кода, отладку и поиск по проекту.

3 Синтаксис default import — без фигурных скобок

Главное преимущество default export — вы импортируете его без фигурных скобок и можете назвать как угодно:

// Все три варианта эквивалентны:
import calculateTotal from './math';
import calcTotal from './math';
import total from './math';

// Все ссылаются на одну и ту же функцию
console.log(calculateTotal(10, 5));  // 50
console.log(calcTotal(10, 5));       // 50
console.log(total(10, 5));           // 50

Вы можете назвать default импорт как хотите — TypeScript не проверяет, совпадает ли имя с оригиналом. Это удобно, но и опасно: если в проекте много файлов с default импортами, становится трудно понять, откуда берётся функция.

Смешанный импорт: Можно импортировать и default, и именованные экспорты одновременно:

// math.ts
export default function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export const PI = 3.14159;
// app.ts
import add, { subtract, PI } from './math';

console.log(add(2, 3));       // 5
console.log(subtract(10, 4)); // 6
console.log(PI);              // 3.14159

Обратите внимание: default импорт идёт без фигурных скобок и отделён запятой от именованных. Порядок не важен — можно сначала именованные, потом default:

// Все варианты эквивалентны:
import add, { subtract, PI } from './math';
import { subtract, PI }, add from './math';  // ❌ Синтаксическая ошибка
import { subtract } from './math';
import add from './math';
import { PI } from './math';

4 Когда использовать default export — правила и рекомендации

Default export хорош в следующих случаях:

  • Компоненты React/Vue: каждый файл — один компонент, который является основным экспортом. Это стандартная конвенция в React-экосистеме.
  • Утилитные модули с одной главной функцией: модуль для форматирования дат, модуль для валидации, модуль для шифрования.
  • Конфигурационные объекты: файл config.ts экспортирует объект настроек.
  • Точка входа в библиотеку: главный модуль, который реэкспортирует всё необходимое.
  • Фабричные функции: функция, которая создаёт и возвращает объект определённого типа.

Не используйте default export, когда:

  • Модуль экспортирует несколько равнозначных сущностей
  • Вы хотите явно показать, что именно импортируется
  • Работаете в команде и хотите обеспечить консистентность кода
  • Используете tree-shaking и хотите минимизировать размер бандла
  • Модуль является утилитарным — содержит много функций

Пример: Модуль с несколькими функциями — лучше именованные экспорты:

// ❌ Не рекомендуется — неочевидно, что импортируется
export default { add, subtract, multiply };

// ✅ Рекомендуется — явно видно, что импортируется
export { add, subtract, multiply };

Правило большого пальца: Если модуль экспортирует одну главную сущность — используйте default. Если экспортирует несколько — используйте именованные.

5 Реэкспорт default export — переотправка

Иногда вам нужно переэкспортировать default export из другого модуля. Это делается через специальный синтаксис:

// utils/index.ts — barrel file
export { default } from './math';
// Теперь default export из math.ts доступен как default из index.ts

Если вы хотите переименовать default export при реэкспорте:

// utils/index.ts
export { default as calculate } from './math';
// Теперь это именованный экспорт

Или переэкспортировать всё из модуля (но default не включается):

// utils/index.ts
export * from './math';
export * from './dates';
// Default export НЕ переэкспортируется через export *

Чтобы переэкспортировать default вместе с именованными:

// utils/index.ts
export { default } from './math';
export * from './math';  // Именованные экспорты
export * from './dates';

Важно: export * не включает default export! Чтобы его переэкспортировать, используйте export { default } from.

⚠️ Типичные ошибки при работе с default export

1. Два default export в одном файле:

// ❌ Ошибка — два default export
export default function add() { /* ... */ }
export default function subtract() { /* ... */ }  // Ошибка!

В модуле может быть только один default export. Если нужно экспортировать несколько функций — используйте именованные экспорты.

2. Импорт default как именованного:

// ❌ Неправильно — добавили фигурные скобки
import { add } from './math';  // add — это default export

// ✅ Правильно — без фигурных скобок
import add from './math';

// ✅ Или импортируем default под именем
import myAdd from './math';

Если вы используете фигурные скобки, TypeScript ищет именованный экспорт с именем add. Если он не найден — будет ошибка или undefined.

3. Забыли, что default можно импортировать под любым именем:

// math.ts
export default function add(a: number, b: number): number {
  return a + b;
}

// app.ts — все три варианта рабочие
import add from './math';        // ✅
import myAdd from './math';      // ✅
import sum from './math';        // ✅

// Но только первый вариант очевиден — остальные запутывают

Совет: Всегда давайте default импорту имя, совпадающее с оригиналом. Это упрощает навигацию по коду и поиск по проекту.

4. Путаница с type-only import:

// ❌ Неправильно — import type для default export
import type User from './models/User';  // User — это класс, не тип

// ✅ Правильно — обычный импорт
import User from './models/User';

// ✅ Правильно — если это интерфейс или тип
import type { User } from './types';

import type используется только для типов, интерфейсов и псевдонимов типов. Если default export — это класс или функция, используйте обычный import.

🛠 Практический пример: React-компонент

Default export особенно популярен в React — каждый файл компонента экспортирует его как default:

// components/Button.tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
  const className = `btn btn-${variant}`;

  return (
    <button
      className={className}
      onClick={onClick}
      disabled={disabled}
      style={{
        padding: '10px 20px',
        borderRadius: '8px',
        border: 'none',
        cursor: disabled ? 'not-allowed' : 'pointer',
        backgroundColor: variant === 'primary' ? '#3b82f6' : '#6b7280',
        color: 'white'
      }}
    >
      {label}
    </button>
  );
}

export default Button;
// components/index.ts — barrel file
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';
export { default as TextField } from './TextField';

Теперь компонент можно импортировать двумя способами:

// Способ 1: через barrel file (именованный экспорт)
import { Button } from './components';

// Способ 2: напрямую (default экспорт)
import Button from './components/Button';

⚖️ Именованные vs Default — итоговое сравнение

Named exports:

  • Можно экспортировать сколько угодно из одного модуля
  • Имя при импорте должно совпадать с экспортом (или использовать alias)
  • Помогают с tree-shaking — бандлер знает, что именно используется
  • Явно показывают, что именно импортируется
  • Лучше для библиотек и больших проектов
  • Код более предсказуемый — нет сюрпризов

Default export:

  • Только один default на модуль
  • Импортируется без фигурных скобок, можно назвать как угодно
  • Не помогает с tree-shaking — бандлер не знает, что именно используется
  • Удобен для компонентов и модулей с одной главной сущностью
  • Популярен в React-экосистеме
  • Код короче — не нужны фигурные скобки

7 Default export и TypeScript — типизация и автодополнение

TypeScript отлично работает с default export. Когда вы импортируете default export, TypeScript автоматически определяет его тип:

// utils.ts
export default function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

// app.ts
import calculateTotal from './utils';

// TypeScript знает типы:
const result = calculateTotal(10, 5);  // result: number
const wrong = calculateTotal('10', 5); // ❌ Ошибка типа

Типизация default export класса:

// User.ts
export default class User {
  constructor(
    public name: string,
    public email: string,
    public age: number
  ) {}

  getInfo(): string {
    return `${this.name} (${this.email}), ${this.age} лет`;
  }
}

// app.ts
import User from './User';

const user = new User('Алексей', 'alex@example.com', 25);
console.log(user.getInfo());  // ✅ TypeScript знает метод getInfo

Типизация default export интерфейса:

// types.ts
export default interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}

// app.ts
import Config from './types';

const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

TypeScript обеспечивает полную типизацию для default export — автодополнение, проверку типов, рефакторинг. Это одна из причин, почему TypeScript так популярен.

8 Default export в других фреймворках — Angular, Vue

Default export используется не только в React. Вот как он используется в других фреймворках:

Vue:

<!-- components/MyButton.vue -->
<template>
  <button @click="handleClick">
    {{ label }}
  </button>
</template>

<script setup lang="ts">
defineProps<{
  label: string;
}>();

const handleClick = () => {
  console.log('Clicked!');
};
</script>
// Импорт компонента
import MyButton from './components/MyButton.vue';

Angular:

// user.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export default class UserService {
  getUser(id: number) {
    return { id, name: 'Алексей' };
  }
}

Во всех фреймворках default export используется для основного экспорта файла — компонента, сервиса, модуля.

✅ Итого

  • export default — экспорт по умолчанию, один на модуль
  • import name from './module' — импорт default без фигурных скобок
  • Default export можно импортировать под любым именем
  • Можно комбинировать с именованными: import def, { named1, named2 } from './module'
  • Реэкспорт default: export { default } from './module'
  • Хорош для компонентов и модулей с одной главной сущностью
  • Используйте именованные экспорты для библиотек и больших проектов
  • Не используйте анонимные default export — давайте имена

9 Default export и tree-shaking — влияние на размер бандла

Default export влияет на tree-shaking — процесс удаления неиспользуемого кода. Вот как это работает:

// utils.ts
export default function heavyFunction() {
  // Тяжёлая функция
  return 'result';
}

export function lightFunction() {
  return 'light';
}

// app.ts — импортируем только lightFunction
import { lightFunction } from './utils';

Бандлер не может удалить heavyFunction, потому что default export — это неявная ссылка. Бандлер не знает, используете ли вы его где-то ещё.

С именованными экспортами tree-shaking работает лучше:

// utils.ts
export function heavyFunction() {
  return 'result';
}

export function lightFunction() {
  return 'light';
}

// app.ts — импортируем только lightFunction
import { lightFunction } from './utils';

Теперь бандлер может удалить heavyFunction, потому что видит явную ссылку только на lightFunction.

10 Default export и тестирование — как тестировать компоненты

При тестировании default export важно правильно импортировать компонент или функцию:

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

function Button({ label, onClick }: ButtonProps) {
  return (
    <button onClick={onClick}>{label}</button>
  );
}

export default Button;

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';  // Импорт default export

describe('Button', () => {
  it('renders with label', () => {
    render(<Button label="Нажми меня" onClick={() => {}} />);
    expect(screen.getByText('Нажми меня')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button label="Нажми меня" onClick={handleClick} />);
    fireEvent.click(screen.getByText('Нажми меня'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

11 Default export и рефакторинг — безопасное переименование

При рефакторинге default export безопаснее переходить на именованные экспорты:

// До рефакторинга
export default function processData(data: any) {
  // ...
}

// После рефакторинга — экспорт по имени
export function processData(data: any) {
  // ...
}

// Все файлы, которые импортировали default:
import processData from './utils';

// Теперь нужно импортировать по имени:
import { processData } from './utils';

IDE помогут автоматически обновить все импорты при рефакторинге. Используйте функцию «Rename Symbol» в вашей IDE.

12 Default export и React Router — маршрутизация

React Router также использует default export для страниц:

// pages/Home.tsx
export default function Home() {
  return <h1>Главная страница</h1>;
}

// pages/About.tsx
export default function About() {
  return <h1>О нас</h1>;
}

// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Проверяем понимание

5 вопросов

Модуль с default экспортом

Premium

Default экспорт класса

Premium