Table of Contents

Naming

Variables, Functions, and Properties

Use camelCase for variable, function, and property names.

const companyName = "pelagornis";
let statusCode = 200;

function fetchCompanyInfo() {
  return { companyName, statusCode };
}

Function names should start with a verb and clearly indicate the function’s purpose.

function fetchUserData(id: number) {
  /* ... */
}

Classes and Interfaces

Use PascalCase for class and interface names.

class User {
  constructor(public id: number, public name: string) {}
}

interface UserData {
  id: number;
  name: string;
  email: string;
}

Constants

Use UPPER_SNAKE_CASE for constants to make it clear they are constant values.

const BASE_URL = "https://api.pelagornis.com";

Enums

Use PascalCase for enum names and UPPER_SNAKE_CASE for enum values.

enum UserRule {
  ADMIN = "ADMIN",
  USER = "USER",
  GUEST = "GUEST",
}

Type Aliases

Use PascalCase for type aliases, appending the Type suffix.

type ApiResponse = { data: TokenType[]; error?: string };

TypeScript Style Rules

Strict Mode

Always enable TypeScript’s strict mode ("strict": true setting). This enforces a stricter type-checking policy to ensure safer code.

Always Use const or let

Never use var. Always use const or let for variable declarations.

const planets = "Earth"; // constant value
let star = "Sun"; // value that might change
star = "Regulus"; // updating value

Avoid any

Avoid using the any type. Instead, use more specific types or, if necessary, unknown.

let result: unknown;

Prefer Explicit Return Types

Always specify an explicit return type for functions.

const fetchData = async (url: string): Promise<string> => {
  const response = await fetch(url);
  return response.text();
};

Avoid Using Function Type

Avoid using the Function type. Instead, use a specific function signature.

type AddFunction = (a: number, b: number) => number;
const add: AddFunction = (a, b) => a + b;

Use as const for Literal Types

Use as const when dealing with constant values to infer literal types.

const direction = "up" as const;

TypeScript Formatting Rules

Indentation

Use 2 spaces for indentation.

Line Length

Limit the length of a line to 100 characters or fewer.

Braces

Always use braces for control structures, even if the block is a single line.

if (isActive) {
  console.log("Active");
}

Semicolons

Always include semicolons (;) at the end of statements.

const statusCode = 200;

Spacing

Add spaces around operators for readability.

const result = a + b;
const product = price * quantity;

Trailing Commas

Use trailing commas in array and object literals.

const companyInfo = {
  name: "pelagornis",
  city: "Seoul, Korea",
};

const numbers = [1, 2, 3];

Advanced TypeScript Features

Union and Intersection Types

Use union and intersection types for flexible type definitions.

// Union types
type Status = "loading" | "success" | "error";
type ID = string | number;

// Intersection types
interface User {
  name: string;
  email: string;
}

interface Admin {
  permissions: string[];
}

type AdminUser = User & Admin;

// Discriminated unions
interface LoadingState {
  status: "loading";
}

interface SuccessState {
  status: "success";
  data: any;
}

interface ErrorState {
  status: "error";
  error: string;
}

type AppState = LoadingState | SuccessState | ErrorState;

function handleState(state: AppState) {
  switch (state.status) {
    case "loading":
      console.log("Loading...");
      break;
    case "success":
      console.log("Data:", state.data);
      break;
    case "error":
      console.log("Error:", state.error);
      break;
  }
}

Mapped Types

Use mapped types for transforming existing types.

// Basic mapped type
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Readonly mapped type
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Custom mapped types
type Stringify<T> = {
  [K in keyof T]: string;
};

type User = {
  id: number;
  name: string;
  age: number;
};

type StringifiedUser = Stringify<User>;
// Result: { id: string; name: string; age: string; }

// Conditional mapped types
type NonNullable<T> = T extends null | undefined ? never : T;

// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvent = EventName<"click" | "hover">; // 'onClick' | 'onHover'

Conditional Types

Use conditional types for type-level logic.

// Basic conditional type
type IsArray<T> = T extends any[] ? true : false;

// Infer keyword
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringArray = string[];
type StringElement = ArrayElement<StringArray>; // string

// Recursive conditional types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Utility conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

Template Literal Types

Use template literal types for string manipulation.

// Basic template literal types
type Greeting = `Hello, ${string}`;
type EventName<T extends string> = `on${Capitalize<T>}`;

// Advanced template literal types
type Join<K, P> = K extends string | number
  ? P extends string | number
    ? K extends ""
      ? P
      : P extends ""
      ? K
      : `${K}${"" extends P ? "" : "."}${P}`
    : never
  : never;

type Paths<T> = T extends object
  ? {
      [K in keyof T]-?: K extends string | number
        ? `${K}` | Join<K, Paths<T[K]>>
        : never;
    }[keyof T]
  : "";

type User = {
  name: string;
  address: {
    street: string;
    city: string;
  };
};

type UserPaths = Paths<User>; // 'name' | 'address' | 'address.street' | 'address.city'

Branded Types

Use branded types for type safety.

// Branded type
type UserId = number & { readonly __brand: unique symbol };
type ProductId = number & { readonly __brand: unique symbol };

function createUserId(id: number): UserId {
  return id as UserId;
}

function createProductId(id: number): ProductId {
  return id as ProductId;
}

// Usage
const userId = createUserId(123);
const productId = createProductId(456);

// This will cause a TypeScript error
// const comparison = userId === productId; // Error!

Generics

Basic Generics

Use generics for reusable type-safe code.

// Generic function
function identity<T>(arg: T): T {
  return arg;
}

// Generic interface
interface Container<T> {
  value: T;
  getValue(): T;
  setValue(value: T): void;
}

// Generic class
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

// Usage
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);

Generic Constraints

Use generic constraints to limit generic types.

// Basic constraint
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// Keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Multiple constraints
interface Serializable {
  serialize(): string;
}

interface Deserializable {
  deserialize(data: string): void;
}

function processData<T extends Serializable & Deserializable>(data: T): T {
  const serialized = data.serialize();
  const newData = data.deserialize(serialized);
  return newData;
}

Utility Types

Use built-in utility types for common transformations.

// Partial - makes all properties optional
interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// Result: { id?: number; name?: string; email?: string; }

// Required - makes all properties required
type RequiredUser = Required<PartialUser>;

// Pick - select specific properties
type UserName = Pick<User, "name">;
// Result: { name: string; }

// Omit - exclude specific properties
type UserWithoutId = Omit<User, "id">;
// Result: { name: string; email: string; }

// Record - create object type with specific keys and values
type UserRoles = Record<string, string[]>;
// Result: { [key: string]: string[]; }

// Exclude - exclude types from union
type NonString = Exclude<string | number | boolean, string>;
// Result: number | boolean

// Extract - extract types from union
type StringOrNumber = Extract<string | number | boolean, string | number>;
// Result: string | number

Advanced Generic Patterns

Use advanced generic patterns for complex type transformations.

// Conditional generic types
type ApiResponse<T> = T extends string
  ? { message: T }
  : T extends number
  ? { count: T }
  : { data: T };

// Recursive generics
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Generic function overloads
function process<T extends string>(input: T): T;
function process<T extends number>(input: T): T;
function process<T>(input: T): T {
  return input;
}

// Generic type guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isArray<T>(value: unknown): value is T[] {
  return Array.isArray(value);
}

// Generic error handling
class ApiError<T = string> extends Error {
  constructor(
    message: string,
    public code: T,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "ApiError";
  }
}

type ErrorCode = "VALIDATION_ERROR" | "NOT_FOUND" | "UNAUTHORIZED";
const error = new ApiError("User not found", "NOT_FOUND", 404);

Decorators and Metadata

Class Decorators

Use class decorators for cross-cutting concerns.

// Basic class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class User {
  name: string;
  email: string;
}

// Decorator factory
function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

// Method decorator
function log(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with args:`, args);
    const result = method.apply(this, args);
    console.log(`${propertyName} returned:`, result);
    return result;
  };
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

Property Decorators

Use property decorators for validation and metadata.

// Property decorator
function required(target: any, propertyKey: string) {
  const existingRequired = Reflect.getMetadata("required", target) || [];
  Reflect.defineMetadata(
    "required",
    [...existingRequired, propertyKey],
    target
  );
}

// Validation decorator
function validate(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const required = Reflect.getMetadata("required", target.constructor) || [];

    for (const prop of required) {
      if (!this[prop]) {
        throw new Error(`Property ${prop} is required`);
      }
    }

    return method.apply(this, args);
  };
}

class User {
  @required
  name: string;

  @required
  email: string;

  @validate
  save(): void {
    console.log("Saving user...");
  }
}

Parameter Decorators

Use parameter decorators for dependency injection and validation.

// Parameter decorator
function inject(
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  const existingTokens = Reflect.getMetadata("design:paramtypes", target) || [];
  const token = existingTokens[parameterIndex];

  // Store injection token
  const existingInjections = Reflect.getMetadata("injections", target) || [];
  existingInjections[parameterIndex] = token;
  Reflect.defineMetadata("injections", existingInjections, target);
}

// Service decorator
function service(target: any) {
  Reflect.defineMetadata("service", true, target);
}

@service
class UserService {
  getUsers(): string[] {
    return ["John", "Jane"];
  }
}

@service
class EmailService {
  sendEmail(to: string, subject: string): void {
    console.log(`Sending email to ${to}: ${subject}`);
  }
}

class UserController {
  constructor(
    @inject private userService: UserService,
    @inject private emailService: EmailService
  ) {}

  notifyUsers(): void {
    const users = this.userService.getUsers();
    users.forEach((user) => {
      this.emailService.sendEmail(user, "Notification");
    });
  }
}

Testing

Unit Testing with Jest

Write comprehensive unit tests for TypeScript code.

// User service
class UserService {
  constructor(private apiClient: ApiClient) {}

  async getUser(id: number): Promise<User> {
    const response = await this.apiClient.get(`/users/${id}`);
    return response.data;
  }

  async createUser(userData: CreateUserData): Promise<User> {
    const response = await this.apiClient.post("/users", userData);
    return response.data;
  }
}

// Test file
describe("UserService", () => {
  let userService: UserService;
  let mockApiClient: jest.Mocked<ApiClient>;

  beforeEach(() => {
    mockApiClient = {
      get: jest.fn(),
      post: jest.fn(),
    } as jest.Mocked<ApiClient>;

    userService = new UserService(mockApiClient);
  });

  describe("getUser", () => {
    it("should return user data", async () => {
      const mockUser: User = { id: 1, name: "John", email: "john@example.com" };
      mockApiClient.get.mockResolvedValue({ data: mockUser });

      const result = await userService.getUser(1);

      expect(mockApiClient.get).toHaveBeenCalledWith("/users/1");
      expect(result).toEqual(mockUser);
    });

    it("should throw error when user not found", async () => {
      mockApiClient.get.mockRejectedValue(new Error("User not found"));

      await expect(userService.getUser(999)).rejects.toThrow("User not found");
    });
  });

  describe("createUser", () => {
    it("should create new user", async () => {
      const userData: CreateUserData = {
        name: "Jane",
        email: "jane@example.com",
      };
      const mockUser: User = { id: 2, ...userData };
      mockApiClient.post.mockResolvedValue({ data: mockUser });

      const result = await userService.createUser(userData);

      expect(mockApiClient.post).toHaveBeenCalledWith("/users", userData);
      expect(result).toEqual(mockUser);
    });
  });
});

Integration Testing

Write integration tests for complete workflows.

// Integration test
describe("User Management Integration", () => {
  let app: Express;
  let server: Server;

  beforeAll(async () => {
    app = createApp();
    server = app.listen(0);
  });

  afterAll(async () => {
    server.close();
  });

  it("should create and retrieve user", async () => {
    const userData = { name: "John Doe", email: "john@example.com" };

    // Create user
    const createResponse = await request(app)
      .post("/api/users")
      .send(userData)
      .expect(201);

    const userId = createResponse.body.id;

    // Retrieve user
    const getResponse = await request(app)
      .get(`/api/users/${userId}`)
      .expect(200);

    expect(getResponse.body).toMatchObject(userData);
  });
});

Mocking and Stubbing

Use mocking for testing with dependencies.

// Mock factory
function createMockUserService(): jest.Mocked<UserService> {
  return {
    getUser: jest.fn(),
    createUser: jest.fn(),
    updateUser: jest.fn(),
    deleteUser: jest.fn(),
  } as jest.Mocked<UserService>;
}

// Test with mocks
describe("UserController", () => {
  let userController: UserController;
  let mockUserService: jest.Mocked<UserService>;

  beforeEach(() => {
    mockUserService = createMockUserService();
    userController = new UserController(mockUserService);
  });

  it("should handle user creation", async () => {
    const userData = { name: "John", email: "john@example.com" };
    const createdUser = { id: 1, ...userData };

    mockUserService.createUser.mockResolvedValue(createdUser);

    const result = await userController.createUser(userData);

    expect(mockUserService.createUser).toHaveBeenCalledWith(userData);
    expect(result).toEqual(createdUser);
  });
});

Performance

Type Optimization

Optimize types for better performance.

// Use const assertions for better inference
const colors = ["red", "green", "blue"] as const;
type Color = (typeof colors)[number]; // 'red' | 'green' | 'blue'

// Use mapped types efficiently
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserUpdate = Optional<User, "id">;

// Use branded types for performance
type UserId = number & { readonly __brand: "UserId" };
type ProductId = number & { readonly __brand: "ProductId" };

// Avoid complex conditional types in hot paths
type SimpleUser = {
  id: number;
  name: string;
  email: string;
};

// Use interfaces for object shapes
interface UserRepository {
  findById(id: UserId): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: UserId): Promise<void>;
}

Memory Management

Optimize memory usage in TypeScript applications.

// Use WeakMap for private data
const privateData = new WeakMap<User, { password: string }>();

class User {
  constructor(public id: number, public name: string, password: string) {
    privateData.set(this, { password });
  }

  getPassword(): string {
    return privateData.get(this)?.password || "";
  }
}

// Use WeakSet for tracking
const processedUsers = new WeakSet<User>();

function processUser(user: User): void {
  if (processedUsers.has(user)) {
    return;
  }

  // Process user
  processedUsers.add(user);
}

// Proper cleanup
class ResourceManager {
  private resources = new Set<{ dispose(): void }>();

  add<T extends { dispose(): void }>(resource: T): T {
    this.resources.add(resource);
    return resource;
  }

  dispose(): void {
    this.resources.forEach((resource) => resource.dispose());
    this.resources.clear();
  }
}

Bundle Optimization

Optimize TypeScript for smaller bundles.

// Use tree-shaking friendly exports
export { UserService } from "./services/UserService";
export { EmailService } from "./services/EmailService";

// Avoid barrel exports in production
// Instead of: export * from './utils';
export { formatDate } from "./utils/date";
export { validateEmail } from "./utils/validation";

// Use dynamic imports for code splitting
async function loadUserModule() {
  const { UserService } = await import("./services/UserService");
  return new UserService();
}

// Use const enums for better performance
const enum UserRole {
  ADMIN = "admin",
  USER = "user",
  GUEST = "guest",
}

Documentation

Document TypeScript code effectively.

/**
 * Represents a user in the system
 * @interface User
 */
interface User {
  /** Unique identifier for the user */
  id: number;
  /** User's full name */
  name: string;
  /** User's email address */
  email: string;
  /** User's role in the system */
  role: UserRole;
}

/**
 * User management service
 * @class UserService
 */
class UserService {
  /**
   * Creates a new user service instance
   * @param apiClient - API client for making HTTP requests
   */
  constructor(private apiClient: ApiClient) {}

  /**
   * Retrieves a user by their ID
   * @param id - The user ID
   * @returns Promise that resolves to the user data
   * @throws {ApiError} When user is not found
   * @example
   * ```typescript
   * const userService = new UserService(apiClient);
   * const user = await userService.getUser(123);
   * console.log(user.name);
   * ```
   */
  async getUser(id: number): Promise<User> {
    // Implementation
  }

  /**
   * Creates a new user
   * @param userData - Data for creating the user
   * @returns Promise that resolves to the created user
   * @throws {ValidationError} When user data is invalid
   * @throws {ApiError} When creation fails
   */
  async createUser(userData: CreateUserData): Promise<User> {
    // Implementation
  }
}