Type Implementation Guide - tonglam/letletme_data GitHub Wiki

Overview

This guide outlines the standardized approach for implementing domain types in our codebase. We follow a functional programming approach using fp-ts, zod for validation, and strict TypeScript typing.

File Structure

Each domain type file should follow this structure:

// ============ Branded Types ============
// ============ Types ============
// ============ Schemas ============
// ============ Type Transformers ============
// ============ Repository Interface ============
// ============ Persistence Types ============
// ============ Converters ============

Detailed Implementation Guide

1. Branded Types

Use branded types for domain-specific identifiers to ensure type safety:

import { Branded, createBrandedType } from './base.type';

export type TeamId = Branded<number, 'TeamId'>;

export const TeamId = createBrandedType<number, 'TeamId'>(
  'TeamId',
  (val): val is number => typeof val === 'number' && val > 0 && Number.isInteger(val),
);

2. Types

Define three main type categories:

2.1 API Response Types (snake_case)

export interface TeamResponse {
  readonly id: number;
  readonly short_name: string;
  // ... other snake_case properties
}

2.2 Domain Types (camelCase)

export interface Team {
  readonly id: TeamId;
  readonly shortName: string;
  // ... other camelCase properties
}

export type Teams = readonly Team[];

2.3 Persistence Types

export interface PrismaTeam {
  readonly id: number;
  readonly shortName: string;
  readonly createdAt: Date;
  // ... other properties
}

export interface PrismaTeamCreate extends Omit<PrismaTeam, 'id' | 'createdAt'> {
  readonly id?: number;
  readonly createdAt: Date;
}

3. Schemas

Use Zod for validation:

export const TeamResponseSchema = z.object({
  id: z.number(),
  short_name: z.string(),
  // ... other validations
});

export const TeamsResponseSchema = z.array(TeamResponseSchema);

4. Type Transformers

Implement pure functions for type transformation:

export const toDomainTeam = (raw: TeamResponse): E.Either<string, Team> =>
  pipe(
    TeamId.validate(raw.id),
    E.map((id) => ({
      id,
      shortName: raw.short_name,
      // ... transform other properties
    })),
  );

5. Repository Interface

Use the base repository interface:

export type TeamRepository = BaseRepository<PrismaTeam, PrismaTeamCreate, TeamId>;

6. Converters

Implement converters between different type representations:

export const convertPrismaTeams = (teams: readonly PrismaTeam[]): TE.TaskEither<APIError, Teams> =>
  pipe(
    teams,
    TE.traverseArray((team) =>
      pipe(
        prismaToResponse(team),
        toDomainTeam,
        TE.fromEither,
        TE.mapLeft((error) => createError('Failed to convert team', error)),
      ),
    ),
  );

export const prismaToResponse = (team: PrismaTeam): TeamResponse => ({
  id: team.id,
  short_name: team.shortName,
  // ... convert other properties
});

Best Practices

1. Type Safety

  • Always use readonly for immutability
  • Use branded types for domain identifiers
  • Avoid type assertions (as) unless absolutely necessary
  • Use strict null checks

2. Validation

  • Use Zod schemas for runtime validation
  • Validate at domain boundaries
  • Handle all possible error cases

3. Transformation

  • Use pure functions for transformations
  • Handle errors using Either
  • Use pipe for function composition
  • Maintain type safety throughout transformations

4. Naming Conventions

  • Response types: snake_case (matching API)
  • Domain types: camelCase
  • Persistence types: camelCase
  • Type names: PascalCase
  • Functions: camelCase

5. Error Handling

  • Use Either for synchronous operations
  • Use TaskEither for asynchronous operations
  • Provide descriptive error messages
  • Handle all edge cases

Example Implementation

See src/types/teams.type.ts for a complete example implementation following these guidelines.

Common Pitfalls to Avoid

  1. Mixing snake_case and camelCase within the same type
  2. Using type assertions instead of proper validation
  3. Not handling null/undefined cases
  4. Forgetting to make properties readonly
  5. Not using branded types for domain identifiers
  6. Direct type casting without validation

Testing

  • Write unit tests for type transformations
  • Test validation edge cases
  • Test error handling
  • Test type conversions

Dependencies

  • fp-ts for functional programming utilities
  • zod for runtime type validation
  • TypeScript for static typing