Skip to content

Naming & File Conventions

A friendly, no-drama guide to how we name things so future-us won’t scream into the void. Follow these and your PRs will glide through CI like butter on a hot skillet.


Why this exists (and why you’ll love it)

  • Consistency: Faster reviews, fewer bikesheds.
  • Discoverability: Filenames tell you what’s inside.
  • Tooling-friendly: ESLint enforces most rules automatically. You code, the robots keep us honest.

Pro tip: Our ESLint setup will complain early and loudly if you drift. That’s a feature.


The TL;DR (sticky-note version)

  • Files: kebab-case by default. Example: user-service.ts.
  • Exception: If a file exports an entity that matches the filename, use that entity’s case instead. Examples: IUserRepository.ts, ProductService.ts, DB_COLLECTIONS.ts, User.ts, apiConfig.ts.
  • Variables/Functions/Methods: camelCase; no leading _.
  • Class properties: private may use leading _; public/protected may not.
  • Parameters: camelCase; leading _ allowed (often for intentionally-unused args).
  • Interfaces: PascalCase. Use I prefix only for service-like abstractions (e.g., IUserService, IOrderRepository). Data shapes like User, Address don’t require I.
  • Classes/Types: PascalCase.
  • Enums: PascalCase; members are UPPER_CASE.
  • Object literal properties: camelCase or snake*case (snake_case is common for DB/config keys). No leading *.
  • Automatic guardrails: A custom ESLint rule errors if a PascalCase filename doesn’t export a matching entity.

Details and examples below.


File naming rules

Base rule: kebab-case for all files

Use kebab-case when the file doesn’t directly mirror a single exported entity name.

ts
// ✅ Good (default)
user - service.ts;
order - repository.ts;
data - helper.ts;
api - utils.ts;

Why: kebab-case is simple, predictable, and plays nicely with most filesystems and tooling.

Exception: filename may match the exported entity’s case

If a file’s primary export’s name matches the filename, the filename may use that export’s case. This makes entity-hunting trivial.

ts
// ✅ Interfaces (service/repo-style)
IUserRepository.ts; // exports: interface IUserRepository
IUserService.ts; // exports: interface IUserService

// ✅ Regular data interfaces
User.ts; // exports: interface User
Address.ts; // exports: interface Address

// ✅ Classes
ProductService.ts; // exports: class ProductService

// ✅ Enums
DB_COLLECTIONS.ts; // exports: enum DB_COLLECTIONS

// ✅ Consts
apiConfig.ts; // exports: const apiConfig

Why: it helps IDE search and code review. When you open ProductService.ts, you know you’ll find ProductService—no surprises.

What’s automatically validated

Our ESLint custom rule enforces this logic:

  • If a filename is PascalCase (e.g., User.ts, ProductService.ts, DB_COLLECTIONS.ts, apiConfig.ts), the file must export an entity with the exact same name.
  • Kebab-case files are not restricted; they can export anything.
  • Test/spec/config files are ignored by this check.

Examples:

ts
// ✅ Pass
// File: ProductService.ts
export class ProductService {}

// ❌ Fail
// File: ProductService.ts
export class OrderService {}
// Fix: Rename the file to OrderService.ts OR rename the class to ProductService.

// ✅ Pass
// File: data-helper.ts
export class AnyName {}

// ❌ Fail
// File: User.ts
export interface User1 {}
// Fix: Rename to User1.ts OR rename interface to User.

Ignored by the rule: *.test.ts, *.spec.ts, *.config.ts.


Code symbol naming rules

These are enforced by @typescript-eslint/naming-convention. The gist:

Variables and functions

ts
// ✅ camelCase
const userName = "john";
function getUserData() {}

// ❌ Not allowed
const _userName = "john"; // leading underscore forbidden
function GetUserData() {} // PascalCase function name forbidden

Why: camelCase is the standard for identifiers in JavaScript/TypeScript.

Methods

ts
class UserService {
    processUser() {}
    // ❌ _processUser() is forbidden (no leading underscore for methods)
}

Why: leading underscores on methods don’t add value and reduce readability.

Class properties

ts
class UserService {
    private _userId: string; // ✅ allowed for private
    private userToken: string; // ✅ also fine
    public userName: string; // ✅ public/protected without underscore
    // ❌ public _userName: string; // not allowed
}

Why: many codebases use a leading _ to hint “private”. We allow it only where it helps (private), forbid it where it confuses (public/protected).

Parameters

ts
function processUser(userId: string, _debug?: boolean) {}

Why: leading _ on params is allowed—useful to signal intentional non-use in implementations.

Interfaces

ts
// ✅ Service/Repository interfaces require I prefix
interface IUserRepository {}
interface IUserService {}
interface IOrderHandler {}

// ✅ Data interfaces don’t require I
interface User {}
interface Address {}
// `IUser` is allowed but optional for data shapes

Why: the I prefix communicates an abstraction boundary (service/repo/handler/…); plain data shapes read nicer without it.

Classes and types

ts
class UserService {}
type UserRole = "admin" | "user";
// ❌ class _UserService {} // leading underscore forbidden

Why: PascalCase for exported types/classes is idiomatic and visually distinct.

Enums and members

ts
enum UserStatus {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
}

// Or with string members in UPPER_CASE
enum UserStatus {
    ACTIVE = "ACTIVE",
    INACTIVE = "INACTIVE",
}
// ❌ _PENDING is forbidden (no leading underscore)

Why: PascalCase enum name reads like a type; UPPER_CASE members stand out as discrete constants.

Object literal properties

ts
const config = {
    database_url: "...", // snake_case for DB/config keys is fine
    apiKey: "...", // camelCase is also fine
    // ❌ _secret: "..."  // leading underscore forbidden
};

Why: sometimes you must match external schemas or environment keys (snake_case). Otherwise, camelCase keeps things consistent.


Directory organization (suggested)

This is how things typically shape up. Names here follow the same rules above.

src/
├── common/            # Shared helpers (kebab-case files)
│   ├── repo-provider.ts
│   ├── data-helper.ts
│   └── apiConfig.ts           # exports const apiConfig
├── interfaces/        # Types/interfaces (entity-matching files)
│   ├── User.ts                 # exports interface User
│   ├── IUserService.ts         # exports interface IUserService
│   └── Address.ts              # exports interface Address
├── services/          # Business logic
│   ├── user-service.ts         # general service file
│   └── ProductService.ts       # exports class ProductService
├── enums/             # Enums
│   ├── DB_COLLECTIONS.ts       # exports enum DB_COLLECTIONS
│   └── user-roles.ts           # general enum file
└── index.ts           # Entry point

Why: discoverability. When you open interfaces/User.ts, you know exactly what to expect.


Gotchas and FAQs

  • The filename rule is strict only for PascalCase filenames. Kebab-case files are flexible by design.
  • Tests/specs/config files are ignored by the filename-export match rule.
  • If ESLint reports: “Filename should be either kebab-case or must match exported entity name,” you likely need to rename either the file or the export.
  • Prefer concise files. If a file grows large or exports too many unrelated things, split it—your teammates (and future you) will thank you.

Rationale recap

  • Predictability: One guess -> correct file.
  • Readability: camelCase for most code; PascalCase for types/interfaces/classes; UPPER_CASE for enum members.
  • Interoperability: snake_case allowed for external-facing keys.
  • Automation: ESLint backs these rules, so we keep humans focused on logic, not punctuation.

Quick reference table

ThingConventionExample
General filenameskebab-caseuser-service.ts
Entity-matching filenamesSame case as exportProductService.ts
Variables/FunctionscamelCasegetUserData
MethodscamelCaseprocessUser
Private class propscamelCase, _ allowed_userId
Public/Protected propscamelCase, no _userName
ParameterscamelCase, _ allowed_debug
Interfaces (service-y)PascalCase with I prefixIUserRepository
Data interfacesPascalCase (no I required)User, Address
Classes/TypesPascalCaseUserService, UserRole
EnumsPascalCaseUserStatus
Enum membersUPPER_CASEACTIVE, INACTIVE
Object literal propertiescamelCase or snake_caseapiKey, database_url

If you spot an edge-case we haven’t covered, bring it up in a PR or drop a note in chat—we’ll evolve the guide together. Happy naming! 🎉