12k
All articles

Как организовать определения типов в TypeScript-проекте

Статья описывает организацию определений типов в TypeScript через стратегию colocation: inline-файлы, общие директории и ambient declaration файлы.

OpenReplay Team
OpenReplay Team
Как организовать определения типов в TypeScript-проекте

Большинство проектов на TypeScript начинаются одинаково: единственный файл types.ts, который медленно разрастается до сотен строк. Найти что-либо становится утомительным занятием. Повторное использование типов в разных файлах кажется рискованным. Новые члены команды не знают, где искать.

Хорошая новость в том, что организация типов TypeScript не требует сложной системы. Она требует одного чёткого правила: размещайте типы как можно ближе к месту их использования и перемещайте их только тогда, когда они нужны в другом месте.

Вот как применять это правило на каждом этапе проекта.

Ключевые выводы

  • Размещайте типы как можно ближе к месту их использования и переносите их в общие расположения только когда они нужны нескольким файлам.
  • Используйте файлы .ts для явного экспорта и импорта типов; резервируйте файлы .d.ts строго для ambient-деклараций, таких как переменные окружения или глобальные расширения.
  • Придерживайтесь единообразного соглашения об именовании, например *.types.ts для типов, специфичных для модуля, и barrel-файлов index.ts для чистых, стабильных путей импорта.
  • В монорепозиториях или общих пакетах экспортируйте типы через условие types в package.json exports и держите внутренние типы приватными.

Ключевое решение: где должен находиться тип?

Задайте себе один вопрос: скольким файлам нужен этот тип?

  • Один файл → определите его непосредственно в этом файле
  • Несколько файлов в одном модуле → разместите его в общем файле .types.ts рядом
  • По всему проекту → переместите его в общую директорию src/types/
  • Между пакетами → опубликуйте его из выделенного пакета типов

Эта прогрессия поддерживает определения типов вашего проекта организованными без излишнего усложнения с самого начала.

Паттерн 1: Встроенные типы для локального использования

Если тип используется только в одном файле, определите его там. Отдельный файл не нужен.

// UserCard.tsx
interface UserCardProps {
  name: string
  avatarUrl: string
  role: "admin" | "viewer"
}

export function UserCard({ name, avatarUrl, role }: UserCardProps) {
  // ...
}

Это правильный подход по умолчанию. Преждевременная абстракция создаёт косвенность без выгоды.

Паттерн 2: Совместно размещённые файлы типов для общих типов модуля

Когда два или более компонента в одной папке используют общие типы, извлеките их в совместно размещённый файл .types.ts.

src/components/user/
├── UserCard.tsx
├── UserList.tsx
└── user.types.ts       ← общие типы для этого модуля

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

Паттерн 3: Общая директория types/ для сквозных определений

Когда типы используются в нескольких несвязанных модулях, имеет смысл центральная директория src/types/. Используйте barrel-файл index.ts для поддержания чистых и стабильных импортов.

src/types/
├── index.ts            ← реэкспортирует всё
├── api.types.ts
├── user.types.ts
└── product.types.ts
// src/types/index.ts
export type { ApiResponse, PaginatedResult } from "./api.types"
export type { User, UserRole } from "./user.types"

Потребители импортируют из одного пути: import type { User } from "@/types". Если вы позже реорганизуете внутреннюю структуру, пути импорта не изменятся.

Следует ли использовать .ts или .d.ts для определений типов?

Это один из наиболее распространённых источников путаницы в организации типов TypeScript.

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

Используйте файлы .d.ts преимущественно для ambient-деклараций — ситуаций, когда вам нужно сообщить TypeScript о чём-то, что существует во время выполнения, но не имеет исходного кода TypeScript. Типичные примеры:

  • env.d.ts — объявление переменных import.meta.env для Vite или подобных сборщиков
  • global.d.ts — расширение типов сторонних модулей или объявление глобальных переменных
// env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string
}

Некоторые современные проекты явно настраивают compilerOptions.types для контроля того, какие ambient-типы загружаются, в то время как другие полагаются на автоматическое обнаружение @types. Если вы используете инструментарий на основе сборщика, такой как Vite или Next.js, следуйте его рекомендуемым соглашениям для этих файлов, а не создавайте отдельные глобальные декларации.

Соглашения об именовании, которые масштабируются

Единообразное именование снижает когнитивную нагрузку в команде:

ПаттернПримерСлучай использования
*.types.tsuser.types.tsТипы, специфичные для модуля
index.tstypes/index.tsBarrel-реэкспорт
env.d.tsenv.d.tsОбъявления переменных окружения
global.d.tsglobal.d.tsРасширение типов сторонних библиотек

Избегайте префиксов IUser и суффиксов UserType. Простые имена вроде User и ApiResponse чище и соответствуют современным соглашениям TypeScript.

Организация типов TypeScript в общем пакете

Если вы создаёте библиотеку или работаете в монорепозитории, экспортируйте типы через package.json exports, используя условие types, а не полагаясь на устаревшие паттерны typesVersions.

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  }
}

Держите внутренние типы приватными — не экспортируйте их из точки входа пакета. Только публичная поверхность API должна быть видна потребителям.

Заключение

Начинайте с встроенных типов. Извлекайте в совместно размещённый файл, когда они нужны второму файлу. Переносите в src/types/, когда они действительно сквозные. Используйте файлы .d.ts только для ambient-деклараций, а не как место по умолчанию для всех определений типов.

Цель организации типов TypeScript — не идеальная структура папок, а возможность для следующего разработчика (или будущего вас) находить и повторно использовать типы без трения.

Часто задаваемые вопросы

Следует ли размещать все типы TypeScript в одном файле types.ts?

Нет. Один файл работает на ранних этапах, но становится трудным для навигации по мере роста проекта. Вместо этого держите типы близко к месту их использования. Определяйте их встроенными для использования в одном файле, извлекайте в совместно размещённый файл .types.ts при совместном использовании внутри модуля, и переносите в центральную директорию типов только когда они нужны в несвязанных частях кодовой базы.

В чём разница между файлами .ts и .d.ts для определений типов?

Используйте файлы .ts для типов, которые вы явно экспортируете и импортируете в коде приложения. Используйте файлы .d.ts только для ambient-деклараций, таких как описание переменных окружения или расширение типов сторонних модулей. Размещение обычных типов приложения в файлах .d.ts может вызвать путаницу, потому что эти декларации становятся глобально доступными без явного импорта.

Как следует именовать файлы типов TypeScript?

Широко принятое соглашение — использовать паттерн module-name.types.ts для типов, специфичных для модуля, и barrel-файл index.ts для реэкспорта в общей директории типов. Избегайте венгерской нотации вроде IUser или избыточных суффиксов вроде UserType. Простые, описательные имена, такие как User и ApiResponse, предпочтительны в современных проектах TypeScript.

Как экспортировать типы из общего пакета в монорепозитории?

Используйте условие types внутри поля exports вашего package.json, чтобы указать на скомпилированный файл деклараций. Этот подход более явный и надёжный, чем устаревший паттерн typesVersions. Экспортируйте только типы, которые составляют часть вашего публичного API, и держите внутренние типы приватными, чтобы избежать утечки деталей реализации потребителям.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.