Как организовать определения типов в TypeScript-проекте
Большинство проектов на TypeScript начинаются одинаково: единственный файл types.ts, который медленно разрастается до сотен строк. Найти что-либо становится утомительным занятием. Повторное использование типов в разных файлах кажется рискованным. Новые члены команды не знают, где искать.
Хорошая новость в том, что организация типов TypeScript не требует сложной системы. Она требует одного чёткого правила: размещайте типы как можно ближе к месту их использования и перемещайте их только тогда, когда они нужны в другом месте.
Вот как применять это правило на каждом этапе проекта.
Ключевые выводы
- Размещайте типы как можно ближе к месту их использования и переносите их в общие расположения только когда они нужны нескольким файлам.
- Используйте файлы
.tsдля явного экспорта и импорта типов; резервируйте файлы.d.tsстрого для ambient-деклараций, таких как переменные окружения или глобальные расширения. - Придерживайтесь единообразного соглашения об именовании, например
*.types.tsдля типов, специфичных для модуля, и barrel-файловindex.tsдля чистых, стабильных путей импорта. - В монорепозиториях или общих пакетах экспортируйте типы через условие
typesвpackage.jsonexports и держите внутренние типы приватными.
Ключевое решение: где должен находиться тип?
Задайте себе один вопрос: скольким файлам нужен этот тип?
- Один файл → определите его непосредственно в этом файле
- Несколько файлов в одном модуле → разместите его в общем файле
.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". Если вы позже реорганизуете внутреннюю структуру, пути импорта не изменятся.
Discover how at OpenReplay.com.
Следует ли использовать .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.ts | user.types.ts | Типы, специфичные для модуля |
index.ts | types/index.ts | Barrel-реэкспорт |
env.d.ts | env.d.ts | Объявления переменных окружения |
global.d.ts | global.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 — не идеальная структура папок, а возможность для следующего разработчика (или будущего вас) находить и повторно использовать типы без трения.
Часто задаваемые вопросы
Нет. Один файл работает на ранних этапах, но становится трудным для навигации по мере роста проекта. Вместо этого держите типы близко к месту их использования. Определяйте их встроенными для использования в одном файле, извлекайте в совместно размещённый файл .types.ts при совместном использовании внутри модуля, и переносите в центральную директорию типов только когда они нужны в несвязанных частях кодовой базы.
Используйте файлы .ts для типов, которые вы явно экспортируете и импортируете в коде приложения. Используйте файлы .d.ts только для ambient-деклараций, таких как описание переменных окружения или расширение типов сторонних модулей. Размещение обычных типов приложения в файлах .d.ts может вызвать путаницу, потому что эти декларации становятся глобально доступными без явного импорта.
Широко принятое соглашение — использовать паттерн module-name.types.ts для типов, специфичных для модуля, и barrel-файл index.ts для реэкспорта в общей директории типов. Избегайте венгерской нотации вроде IUser или избыточных суффиксов вроде UserType. Простые, описательные имена, такие как User и ApiResponse, предпочтительны в современных проектах TypeScript.
Используйте условие types внутри поля exports вашего package.json, чтобы указать на скомпилированный файл деклараций. Этот подход более явный и надёжный, чем устаревший паттерн typesVersions. Экспортируйте только типы, которые составляют часть вашего публичного API, и держите внутренние типы приватными, чтобы избежать утечки деталей реализации потребителям.
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.
Check our GitHub repo and join the thousands of developers in our community.