Skip to content

TypeScript Standards ​

These TypeScript conventions apply to all code in both the backend (NestJS) and frontend (Nuxt) projects. They ensure type safety, consistency, and readability across the entire codebase.

Quick Reference ​

RuleCorrectIncorrect
Interface prefixIUserDataUserData
Enum suffixUserStatusTypesUserStatus
No anyunknown with type guardany
Return typesfunction(): stringfunction() (inferred)
Optional propsname?: stringname: string | undefined

No any ​

The any type disables type checking and should never be used. Use proper types, unknown with type guards, or generics instead.

typescript
// Use a proper interface
function processData(data: IUserData): string {
  return data.name;
}

// Use unknown + type guard for truly unknown data
function processData(data: unknown): string {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    return (data as { name: string }).name;
  }
  throw new Error('Invalid data');
}

// Use generics for flexible functions
function processData<T extends { name: string }>(data: T): string {
  return data.name;
}
SituationUse Instead of any
Unknown API responseunknown + type guard
Dynamic object keysRecord<string, T>
Multiple possible typesUnion type A | B | C
Complex genericProper generic constraints

Interface Naming ​

All interfaces use the I prefix. This makes it immediately clear what's an interface vs a class, and it's consistent with the gateway pattern where IUserGateway is the abstract contract and UserGateway is the implementation.

typescript
export interface IUserData {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface ICreateUserData {
  email: string;
  name: string;
}

Naming Patterns ​

TypePatternExample
Base entityI{Entity}DataIUserData, IProjectData
Create inputICreate{Entity}DataICreateUserData
Update inputIUpdate{Entity}DataIUpdateUserData
Filter/QueryI{Entity}FilterIUserFilter
GatewayI{Entity}GatewayIUserGateway
With relationsI{Entity}With{Extra}IUserWithTeams

Enum Naming ​

All enums end with the Types suffix and use string values:

typescript
export enum UserStatusTypes {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 'pending',
}

export enum RoleTypes {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest',
}

WARNING

Always use string enum values, not numeric ones. String values are readable in JSON, database records, and logs. Numeric enums are fragile and hard to debug.

Type Aliases vs Interfaces ​

Use interfaces for object shapes and type aliases for unions, utilities, and function signatures:

typescript
// Interfaces for object shapes
export interface IUserData {
  id: string;
  name: string;
}

// Type aliases for everything else
export type UserRole = 'admin' | 'user' | 'guest';
export type UserId = string;
export type Nullable<T> = T | null;
export type UserValidator = (user: IUserData) => boolean;

Return Types ​

Always declare explicit return types for functions and methods:

typescript
function getUser(id: string): IUserData {
  return { id, name: 'John', /* ... */ };
}

async function fetchUser(id: string): Promise<IUserData> {
  return await this.gateway.findById(id);
}

async function findUser(id: string): Promise<IUserData | null> {
  return await this.gateway.findById(id);
}

Optional Properties ​

Use ? for optional properties, not | undefined:

typescript
export interface IUpdateUserData {
  name?: string;
  email?: string;
}

function greet(name: string, title?: string): string {
  return title ? `${title} ${name}` : name;
}

Null vs Undefined ​

Use null for intentional absence (API responses, cleared values) and undefined for unset optional fields:

typescript
// null = intentionally empty
export interface IUserData {
  avatarUrl: string | null;  // User explicitly has no avatar
}

// undefined = not provided
export interface IUpdateUserData {
  name?: string;  // Field not included in update
}

// Function returning nullable
async function findUser(id: string): Promise<IUserData | null> {
  const user = await this.db.find(id);
  return user ?? null;
}

Import Organization ​

Organize imports in this order, separated by blank lines:

typescript
// 1. Framework / Node.js built-ins
import { Injectable } from '@nestjs/common';

// 2. External packages
import { v4 as uuid } from 'uuid';

// 3. Slice aliases (cross-slice imports)
import { PrismaService } from '#prisma';
import { IUserData } from '#user/domain';

// 4. Relative imports (same slice only)
import { UserMapper } from './user.mapper';

Slice Aliases

Always use # aliases for cross-slice imports (#prisma, #user/domain). Use relative paths only within the same slice. This makes dependencies explicit and refactoring safe.

Named Exports ​

Use named exports everywhere. Avoid default exports unless required by a framework (like defineNuxtConfig):

typescript
// Correct — named exports
export interface IUserData { /* ... */ }
export class UserService { /* ... */ }

// Correct — named imports
import { IUserData, UserService } from './user.types';

// Avoid — default exports
export default class UserService { /* ... */ }

Async/Await ​

Always use async/await instead of raw Promise chains:

typescript
// Correct
async function getUser(id: string): Promise<IUserData> {
  const user = await this.gateway.findById(id);
  if (!user) throw new UserNotFoundError(id);
  return user;
}

// Correct — parallel operations
async function getUserWithTeam(userId: string): Promise<IUserWithTeam> {
  const [user, team] = await Promise.all([
    this.userGateway.findById(userId),
    this.teamGateway.findByUserId(userId),
  ]);
  return { ...user, team };
}

Class Properties ​

Use private readonly for injected dependencies:

typescript
@Injectable()
export class UserService {
  constructor(
    private readonly userGateway: IUserGateway,
    private readonly logger: LoggerService,
  ) {}

  async getUser(id: string): Promise<IUserData> {
    return await this.userGateway.findById(id);
  }
}

What's Next? ​

Built with CleanSlice