Skip to content

Naming Conventions

Consistent naming improves code readability and maintainability.

Never Abbreviate an Identifier

Hard rule. Write the whole word - in class names, variables, and type parameters alike.

typescript
// ✅ GOOD
class ProductRepository { }
type TProductDocument = { };
const configuration = resolveConfiguration();
class Repository<TDocument> { }

// ❌ BAD
class ProductRepo { }
type TProductDoc = { };
const cfg = resolveCfg();
class Repository<TDoc> { }

Directory Structure

Component Organization

src/components/[feature]/
├── index.ts              # Barrel exports
├── component.ts          # IoC binding setup
├── controller.ts         # Route handlers
└── common/
    ├── index.ts          # Barrel exports
    ├── keys.ts           # Binding key constants
    ├── types.ts          # Interfaces and types
    └── rest-paths.ts     # Route path constants

Complex Component (with multiple features)

src/components/auth/
├── index.ts
├── authenticate/
│   ├── index.ts
│   ├── component.ts
│   ├── common/
│   ├── controllers/
│   ├── services/
│   └── strategies/
└── models/
    ├── entities/         # Database models
    └── requests/         # Request schemas

Barrel Exports

Every folder should have an index.ts that re-exports its contents:

typescript
// components/health-check/index.ts
export * from './common';
export * from './component';
export * from './controller';

// components/health-check/common/index.ts
export * from './keys';
export * from './rest-paths';
export * from './types';

Class Names

TypePatternExample
Components[Feature]ComponentHealthCheckComponent, AuthComponent
Controllers[Feature]ControllerUserController, AuthController
Services[Feature]ServiceJWSTokenService, PaymentService
Repositories[Feature]RepositoryUserRepository, OrderRepository
Strategies[Feature]StrategyJWSAuthenticationStrategy, BasicAuthenticationStrategy
Factories[Feature]FactoryUIProviderFactory

File Names

Both styles are acceptable: [type].ts or [name].[type].ts

TypeSingle FileMultiple Files
Componentscomponent.tsauth.component.ts
Controllerscontroller.tsuser.controller.ts
Servicesservice.tsjwt-token.service.ts
Repositoriesrepository.tsuser.repository.ts
Types/Interfacestypes.tsuser.types.ts
Constantsconstants.tskeys.ts, rest-paths.ts
Schemasschema.tssign-in.schema.ts

Guidelines:

  • Use [type].ts when there's only one file of that type in the folder
  • Use [name].[type].ts when there are multiple files of the same type
  • Use kebab-case for multi-word names: jwt-token.service.ts
  • A file name never repeats its folder. Name the file for its role inside the folder: logger/winston/logger.ts, not logger/winston/winston-logger.ts

Type and Interface Prefixes

typescript
// Interfaces use 'I' prefix
interface IHealthCheckOptions {
  restOptions: { path: string };
}

interface IAuthService {
  signIn(context: TContext, opts: TSignInRequest): Promise<AnyObject>;
}

// Type aliases use 'T' prefix
type TSignInRequest = z.infer<typeof SignInRequestSchema>;
type TAuthStrategy = TConstValue<typeof AuthenticateStrategy>;

// ❌ Never a bare string-literal union for an enumerable set - it has no runtime
// values to validate against. Use a const class + TConstValue instead.
type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'emerg';

// Generic constraints
type TTableSchemaWithId<TC extends TableConfig = TableConfig> = PgTable<TC> & {
  id: TIdColumn;
};

Binding Keys

Use static class with @app/[component]/[feature] format:

typescript
export class HealthCheckBindingKeys {
  static readonly HEALTH_CHECK_OPTIONS = '@app/health-check/options';
}

export class SocketIOBindingKeys {
  static readonly SOCKET_IO_INSTANCE = '@app/socket-io/instance';
  static readonly SERVER_OPTIONS = '@app/socket-io/server-options';
}

Private Field Naming

Use underscore prefix (_) for private and protected class fields to distinguish them from public fields and method parameters.

typescript
class MyRepository extends DefaultRelationalRepository {
  // Private fields with underscore prefix
  private _dataSource: AbstractDataSource;
  private _entity: AbstractEntity;
  private _hiddenProperties: Set<string> | null = null;

  // Protected fields also use underscore prefix
  protected _schemaFactory?: ReturnType<typeof createSchemaFactory>;

  constructor(dataSource: AbstractDataSource) {
    // 'dataSource' (param) vs '_dataSource' (field)
    this._dataSource = dataSource;
  }
}

Benefits:

  • Clear distinction between fields and parameters
  • Avoids naming conflicts in constructors
  • Consistent with TypeScript community conventions

Sentinel Value Pattern for Caching

Use null to distinguish "not computed" from "computed as undefined" for lazy-initialized cached values.

typescript
class Repository {
  // null = not computed yet, undefined = computed but no value
  private _visibleProperties: Record<string, any> | null | undefined = null;

  get visibleProperties(): Record<string, any> | undefined {
    if (this._visibleProperties !== null) {
      return this._visibleProperties;
    }
    // Compute once and cache (may be undefined)
    this._visibleProperties = this.computeVisibleProperties();
    return this._visibleProperties;
  }
}

Why not just undefined?

  • undefined can be a valid computed result
  • null clearly indicates "never computed"
  • Prevents redundant re-computation

See Also