Organizing types and the @ path alias in TypeScript

Table of Contents

  1. Why separate types into their own files
  2. Folder structures that work
  3. File naming conventions
  4. interface vs type — when to use which
  5. import type — avoid runtime overhead
  6. Barrel exports for clean imports
  7. The @ path alias — what it is and why
  8. Setting up @ in a Vite project
  9. Putting it all together

Why separate types into their own files

↑ Index

A User type might be needed in API calls, React components, services, and tests. Instead of redefining it everywhere, define it once and import it:

// types/user.types.ts
export interface User {
  id: number
  name: string
  email: string
}
// anywhere else
import type { User } from '@/types'

function createUser(user: User) {}

Without separation, you end up with inline types that are hard to read and impossible to reuse:

// ❌ inline — repeated everywhere, hard to maintain
function createUser(user: { id: number; name: string; email: string }) {}

Shared type files also prevent the classic team bug: developer A returns username, developer B expects name. A single shared type catches this at compile time.

Folder structures that work

↑ Index

Global types folder (most common)

Best for small-to-medium projects:

src/
 ├── types/
 │    ├── user.types.ts
 │    ├── order.types.ts
 │    ├── api.types.ts
 │    └── index.ts        ← barrel export
 ├── components/
 ├── services/
 └── utils/

Feature-based (large projects)

Types live next to the code that uses them:

src/
 ├── features/
 │    ├── auth/
 │    │     ├── auth.types.ts
 │    │     ├── auth.service.ts
 │    │     └── auth.controller.ts
 │    └── users/
 │          ├── user.types.ts
 │          ├── user.service.ts
 │          └── user.controller.ts

This keeps everything related together. Larger codebases tend to prefer this because it scales better — each feature is self-contained.

Which to pick?

Start with the global types/ folder. Move to feature-based when you have 10+ type files and they're clearly tied to specific features.

File naming conventions

↑ Index

Three common patterns — pick one and be consistent:

PatternExampleWhen
*.types.tsuser.types.tsInside any folder — explicit and searchable
*.ts inside /typestypes/user.tsWhen the folder already says "types"
types.ts per moduleauth/types.tsFeature-based architecture

Most teams use user.types.ts because it's unambiguous no matter where the file sits.

interface vs type — when to use which

↑ Index

Use interface for object shapes:

export interface User {
  id: number
  name: string
  email: string
}

Use type for unions, intersections, and utility types:

export type Status = 'pending' | 'paid' | 'failed'
export type ID = string | number

Interfaces support declaration merging and extends. Types support unions and mapped types. For most object shapes, either works — just be consistent within a project.

import type — avoid runtime overhead

↑ Index

When you only need a type (not a value), use import type:

import type { User } from '@/types'

This tells the compiler the import is erased at build time — it never appears in the compiled JavaScript output. Benefits:

  • Smaller bundles — no leftover import statements for things that don't exist at runtime
  • Clearer intent — anyone reading the code knows this is a type, not a runtime value
  • Avoids circular dependency issues in some bundler configurations

With verbatimModuleSyntax: true in your tsconfig.app.json (Vite's default), TypeScript requires import type for type-only imports — so this isn't optional.

Barrel exports for clean imports

↑ Index

Suppose this is your structure:

src/
 ├── types/
 │    ├── user.types.ts
 │    ├── order.types.ts
 │    ├── api.types.ts
 │    └── index.ts        ← barrel export
 ├── components/
 ├── services/
 └── utils/

Create an index.ts file in your types folder that re-exports everything:

// types/index.ts
export * from './user.types'
export * from './order.types'
export * from './api.types'

Now imports become shorter:

// ✅ clean — one import source
import type { User, Order } from '@/types'

// ❌ without barrel — separate imports
import type { User } from '@/types/user.types'
import type { Order } from '@/types/order.types'

This also works for components:

// components/index.ts
export { Button } from './Button'
export { Card } from './Card'

// usage
import { Button, Card } from '@/components'

The @ path alias — what it is and why

↑ Index

In many TypeScript projects you see:

import type { User } from '@/types/user'

@ is a path alias that maps to src/. It's not built into TypeScript or JavaScript — you configure it yourself.

Without it, imports from deeply nested files look like this:

import type { User } from '../../../types/user'

These relative paths are fragile (they break when you move files) and hard to read. With @:

import type { User } from '@/types/user'

Always resolves from src/, no matter where the importing file lives.

Setting up @ in a Vite project

↑ Index

You must configure two places — Vite (for the bundler) and TypeScript (for the type checker). If you only do one, the other won't understand the alias.

Step 1 — Vite config

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

This tells Vite: when you see @/..., resolve it from src/.

Step 2 — TypeScript config

Add baseUrl and paths to tsconfig.app.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

This tells the TypeScript compiler (and VS Code's language server) how to resolve @/....

Common mistake

Configuring only Vite but forgetting the TypeScript paths:

Cannot find module '@/components/Button'

The app runs fine (Vite resolves it), but VS Code shows errors because TypeScript doesn't know what @ means. Always configure both.

Stick to one alias

Some teams create multiple aliases (@components, @services, @types). This adds complexity without much benefit. One alias (@src) is enough for most projects.

Putting it all together

↑ Index

A clean Vite + React + TypeScript project structure:

src/
 ├── types/
 │    ├── user.types.ts
 │    ├── api.types.ts
 │    └── index.ts
 ├── services/
 │    └── user.service.ts
 ├── components/
 │    ├── UserCard.tsx
 │    └── index.ts
 └── pages/
      └── Home.tsx

Type file:

// types/user.types.ts
export interface User {
  id: number
  name: string
  email: string
}

Barrel export:

// types/index.ts
export * from './user.types'
export * from './api.types'

Service using the type:

// services/user.service.ts
import type { User } from '@/types'

export async function getUsers(): Promise<User[]> {
  const res = await fetch('/api/users')
  return res.json()
}

Component using both:

// components/UserCard.tsx
import type { User } from '@/types'

export function UserCard({ user }: { user: User }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

The pattern: define types once → barrel export → import with import type and @/ everywhere.