Appearance
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
Iprefix only for service-like abstractions (e.g.,IUserService,IOrderRepository). Data shapes likeUser,Addressdon’t requireI. - 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 apiConfigWhy: 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 forbiddenWhy: 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 shapesWhy: 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 forbiddenWhy: 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 pointWhy: 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
| Thing | Convention | Example |
|---|---|---|
| General filenames | kebab-case | user-service.ts |
| Entity-matching filenames | Same case as export | ProductService.ts |
| Variables/Functions | camelCase | getUserData |
| Methods | camelCase | processUser |
| Private class props | camelCase, _ allowed | _userId |
| Public/Protected props | camelCase, no _ | userName |
| Parameters | camelCase, _ allowed | _debug |
| Interfaces (service-y) | PascalCase with I prefix | IUserRepository |
| Data interfaces | PascalCase (no I required) | User, Address |
| Classes/Types | PascalCase | UserService, UserRole |
| Enums | PascalCase | UserStatus |
| Enum members | UPPER_CASE | ACTIVE, INACTIVE |
| Object literal properties | camelCase or snake_case | apiKey, 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! 🎉