Type Safety Guide

Learn about type safety features and best practices in go-errors

Type Safety Guide

This guide covers type safety features and best practices when using the go-errors library.

Core Types

Result Type

The foundation of type safety in go-errors is the Result type:

type Result<T, E = Error> = readonly [T, null] | readonly [null, E];

This type ensures that:

  1. You always get either a value or an error, never both
  2. The tuple is readonly to prevent accidental modifications
  3. The error type can be customized
  4. TypeScript can infer the correct types

Generic Type Parameters

Basic Usage

import { goSync, goFetch } from 'go-errors';

// Default error type (Error)
let [num, err] = goSync(() => 42);
// num is number | null
// err is Error | null

// Custom value type
let [str, err] = goSync<string>(() => 'hello');
// str is string | null
// err is Error | null

// Custom error type
let [val, err] = goSync<number, ValidationError>(() => {
  throw new ValidationError('value', 'Invalid number');
});
// val is number | null
// err is ValidationError | null

Type Inference

TypeScript can often infer the correct types:

// Type inference from return value
let [num, err] = goSync(() => 42);
// num is inferred as number | null

// Type inference from thrown error
let [val, err] = goSync(() => {
  throw new ValidationError('field', 'message');
});
// err is inferred as ValidationError | null

// Type inference in async operations
let [data, err] = await goFetch('/api/data');
// data is inferred from response

Custom Error Types

Defining Error Types

// Basic custom error
class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AppError';
  }
}

// Error with additional data
class ValidationError extends Error {
  constructor(
    public field: string,
    message: string,
    public code: number = 400
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// Union type for all possible errors
type ApplicationError = 
  | ValidationError 
  | NotFoundError 
  | AuthError;

Using Custom Errors

function validateUser(user: User) {
  let [valid, err] = goSync<boolean, ValidationError>(() => {
    if (!user.name) {
      throw new ValidationError('name', 'Name is required');
    }
    if (!user.email) {
      throw new ValidationError('email', 'Email is required');
    }
    return true;
  });

  // TypeScript knows err is ValidationError | null
  if (err) {
    console.error(`Field ${err.field} is invalid:`, err.message);
    return [null, err] as const;
  }

  return [valid, null] as const;
}

HTTP Types

Response Types

interface ApiResponse<T> {
  data: T;
  metadata: {
    timestamp: string;
    version: string;
  };
}

interface User {
  id: number;
  firstName: string;
  lastName: string;
  fullName: string;
  email: string;
}

// Type-safe API call
let [response, err] = await goFetch<ApiResponse<User>>('/api/user/1', {
  responseTransformer: (data) => ({
    ...data,
    data: {
      ...data.data,
      fullName: `${data.data.firstName} ${data.data.lastName}`
    }
  })
});

if (err) return [null, err] as const;

// TypeScript knows response.data is User
console.log(response.data.fullName);

Error Types

interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

// Type-safe error handling
let [data, err] = await goFetch<User, ApiError>('/api/user/1', {
  errorTransformer: (error) => {
    if (error instanceof Response) {
      return {
        code: `HTTP_${error.status}`,
        message: error.statusText
      };
    }
    return {
      code: 'UNKNOWN_ERROR',
      message: error instanceof Error ? error.message : String(error)
    };
  }
});

if (err) {
  // TypeScript knows err has code and message
  console.error(`API Error ${err.code}: ${err.message}`);
  return;
}

Advanced Type Safety

Discriminated Unions

// Define a discriminated union for different error types
type Result<T> =
  | { type: 'success'; data: T }
  | { type: 'validation'; errors: ValidationError[] }
  | { type: 'notFound'; resource: string }
  | { type: 'auth'; message: string };

function processResult<T>(result: Result<T>) {
  switch (result.type) {
    case 'success':
      // TypeScript knows result.data exists
      return result.data;
    case 'validation':
      // TypeScript knows result.errors exists
      return handleValidationErrors(result.errors);
    case 'notFound':
      // TypeScript knows result.resource exists
      return handleNotFound(result.resource);
    case 'auth':
      // TypeScript knows result.message exists
      return handleAuthError(result.message);
  }
}

Type Guards

// Type guard for custom error
function isValidationError(error: unknown): error is ValidationError {
  return error instanceof Error && 
         'field' in error &&
         typeof (error as any).field === 'string';
}

// Using type guards
let [data, err] = goSync(() => validateData());
if (err) {
  if (isValidationError(err)) {
    // TypeScript knows err has field property
    console.error(`Validation failed for ${err.field}`);
  } else {
    console.error('Unknown error:', err);
  }
  return;
}

Type Safety with Transformers

Response Transformers

interface ApiUser {
  id: string;  // API returns string
  created_at: string;
  updated_at: string;
  first_name: string;
  last_name: string;
  email_address: string;
}

interface User {
  id: number;  // We want number
  createdAt: Date;
  updatedAt: Date;
  firstName: string;
  lastName: string;
  fullName: string;
  email: string;
}

let [user, err] = await goFetch<User>('/api/user/1', {
  responseTransformer: (data: unknown): User => {
    const apiUser = data as ApiUser;
    return {
      id: parseInt(apiUser.id, 10),
      createdAt: new Date(apiUser.created_at),
      updatedAt: new Date(apiUser.updated_at),
      firstName: apiUser.first_name,
      lastName: apiUser.last_name,
      fullName: `${apiUser.first_name} ${apiUser.last_name}`,
      email: apiUser.email_address,
    };
  }
});

Error Transformers

interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

let [data, err] = await goFetch<User, ApiError>('/api/user', {
  errorTransformer: (error) => {
    if (error instanceof Response) {
      return {
        code: `HTTP_${error.status}`,
        message: error.statusText
      };
    }
    if (error instanceof ValidationError) {
      return {
        code: 'VALIDATION_ERROR',
        message: error.message,
        details: { field: error.field }
      };
    }
    return {
      code: 'UNKNOWN_ERROR',
      message: error instanceof Error ? error.message : String(error)
    };
  }
});

Best Practices

  1. Always Use Type Parameters

    // ✅ Good: Explicit types
    let [user, err] = await goFetch<User, ApiError>('/api/user');
    
    // ❌ Bad: Implicit any
    let [data, err] = await goFetch('/api/user');
    
  2. Use Type Guards

    // ✅ Good: Type guard for error handling
    if (isValidationError(err)) {
      console.error(`Field ${err.field} is invalid`);
    }
    
    // ❌ Bad: Type assertion
    console.error(`Field ${(err as ValidationError).field}`);
    
  3. Return Type Safety

    // ✅ Good: Use as const for proper inference
    return [value, null] as const;
    return [null, err] as const;
    
    // ❌ Bad: Without as const
    return [value, null];
    

See Also