Detalhes do pacote

wellcrafted

braden_wong_567MIT0.22.0

Delightful TypeScript patterns for elegant, type-safe applications

typescript, delightful, elegant, type-safe

readme (leia-me)

wellcrafted

npm version TypeScript License: MIT Bundle Size

Delightful TypeScript utilities for elegant, type-safe applications

Transform unpredictable errors into type-safe results

// ❌ Before: Which errors can this throw? 🤷
try {
  await saveUser(user);
} catch (error) {
  // ... good luck debugging in production
}

// ✅ After: Every error is visible and typed
const { data, error } = await saveUser(user);
if (error) {
  switch (error.name) {
    case "ValidationError": 
      showToast(`Invalid ${error.context.field}`);
      break;
    case "AuthError":
      redirectToLogin();
      break;
    // TypeScript ensures you handle all cases!
  }
}

A collection of simple, powerful primitives

🎯 Result Type

Make errors explicit in function signatures

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return Err("Division by zero");
  return Ok(a / b);
}

🏷️ Brand Types

Create distinct types from primitives

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

// TypeScript prevents mixing them up!
function getUser(id: UserId) { /* ... */ }

📋 Tagged Errors

Structured, serializable errors with convenient factory functions

import { createTaggedError } from "wellcrafted/error";

const { ApiError, ApiErr } = createTaggedError("ApiError");
// ApiError() creates error object, ApiErr() creates Err-wrapped error

Installation

npm install wellcrafted

Quick Start

import { tryAsync } from "wellcrafted/result";
import { createTaggedError } from "wellcrafted/error";

// Define your error with factory function
const { ApiError, ApiErr } = createTaggedError("ApiError");
type ApiError = ReturnType<typeof ApiError>;

// Wrap any throwing operation
const { data, error } = await tryAsync({
  try: () => fetch('/api/user').then(r => r.json()),
  mapErr: (error) => ApiErr({
    message: "Failed to fetch user",
    context: { endpoint: '/api/user' },
    cause: error
  })
});

if (error) {
  console.error(`${error.name}: ${error.message}`);
} else {
  console.log("User:", data);
}

Core Features

🎯 Explicit Error Handling
All errors visible in function signatures
📦 Serialization-Safe
Plain objects work everywhere
✨ Elegant API
Clean, intuitive patterns
🔍 Zero Magic
~50 lines of core code
🚀 Lightweight
Zero dependencies, < 2KB
🎨 Composable
Mix and match utilities

The Result Pattern Explained

The Result type makes error handling explicit and type-safe:

// The entire implementation
type Ok<T> = { data: T; error: null };
type Err<E> = { error: E; data: null };
type Result<T, E> = Ok<T> | Err<E>;

The Magic: This creates a discriminated union where TypeScript automatically narrows types:

if (result.error) {
  // TypeScript knows: error is E, data is null
} else {
  // TypeScript knows: data is T, error is null
}

Basic Patterns

Handle Results with Destructuring

const { data, error } = await someOperation();

if (error) {
  // Handle error with full type safety
  return;
}

// Use data - TypeScript knows it's safe

Wrap Unsafe Operations

// Synchronous
const result = trySync({
  try: () => JSON.parse(jsonString),
  mapErr: (error) => Err({
    name: "ParseError",
    message: "Invalid JSON",
    context: { input: jsonString },
    cause: error
  })
});

// Asynchronous  
const result = await tryAsync({
  try: () => fetch(url),
  mapErr: (error) => Err({
    name: "NetworkError",
    message: "Request failed",
    context: { url },
    cause: error
  })
});

Service Layer Example

import { Result, Ok, tryAsync } from "wellcrafted/result";
import { createTaggedError } from "wellcrafted/error";

// Define service-specific errors
const { ValidationError, ValidationErr } = createTaggedError("ValidationError");
const { DatabaseError, DatabaseErr } = createTaggedError("DatabaseError");

type ValidationError = ReturnType<typeof ValidationError>;
type DatabaseError = ReturnType<typeof DatabaseError>;

// Factory function pattern - no classes!
export function createUserService(db: Database) {
  return {
    async createUser(input: CreateUserInput): Promise<Result<User, ValidationError | DatabaseError>> {
      // Direct return with Err variant
      if (!input.email.includes('@')) {
        return ValidationErr({
          message: "Invalid email format",
          context: { field: 'email', value: input.email },
          cause: undefined
        });
      }

      return tryAsync({
        try: () => db.save(input),
        mapErr: (error) => DatabaseErr({
          message: "Failed to save user",
          context: { operation: 'createUser', input },
          cause: error
        })
      });
    },

    async getUser(id: string): Promise<Result<User | null, DatabaseError>> {
      return tryAsync({
        try: () => db.findById(id),
        mapErr: (error) => DatabaseErr({
          message: "Failed to fetch user",
          context: { userId: id },
          cause: error
        })
      });
    }
  };
}

// Export type for the service
export type UserService = ReturnType<typeof createUserService>;

// Create a live instance (dependency injection at build time)
export const UserServiceLive = createUserService(databaseInstance);

Why wellcrafted?

JavaScript's try-catch has fundamental problems:

  1. Invisible Errors: Function signatures don't show what errors can occur
  2. Lost in Transit: JSON.stringify(new Error()) loses critical information
  3. No Type Safety: TypeScript can't help with catch (error) blocks
  4. Inconsistent: Libraries throw different things (strings, errors, objects, undefined)

wellcrafted solves these with simple, composable primitives that make errors:

  • Explicit in function signatures
  • Serializable across all boundaries
  • Type-safe with full TypeScript support
  • Consistent with structured error objects

Service Pattern Best Practices

Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:

Factory Function Pattern

import { createTaggedError } from "wellcrafted/error";

// 1. Define service-specific errors
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError");
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;

// 2. Create service with factory function
export function createRecorderService() {
  // Private state in closure
  let isRecording = false;

  // Return object with methods
  return {
    startRecording(): Result<void, RecorderServiceError> {
      if (isRecording) {
        return RecorderServiceErr({
          message: "Already recording",
          context: { isRecording },
          cause: undefined
        });
      }

      isRecording = true;
      return Ok(undefined);
    },

    stopRecording(): Result<Blob, RecorderServiceError> {
      if (!isRecording) {
        return RecorderServiceErr({
          message: "Not currently recording", 
          context: { isRecording },
          cause: undefined
        });
      }

      isRecording = false;
      return Ok(new Blob(["audio data"]));
    }
  };
}

// 3. Export type
export type RecorderService = ReturnType<typeof createRecorderService>;

// 4. Create singleton instance
export const RecorderServiceLive = createRecorderService();

Platform-Specific Services

For services that need different implementations per platform:

// types.ts - shared interface
export type FileService = {
  readFile(path: string): Promise<Result<string, FileServiceError>>;
  writeFile(path: string, content: string): Promise<Result<void, FileServiceError>>;
};

// desktop.ts
export function createFileServiceDesktop(): FileService {
  return {
    async readFile(path) {
      // Desktop implementation using Node.js APIs
    },
    async writeFile(path, content) {
      // Desktop implementation
    }
  };
}

// web.ts  
export function createFileServiceWeb(): FileService {
  return {
    async readFile(path) {
      // Web implementation using File API
    },
    async writeFile(path, content) {
      // Web implementation
    }
  };
}

// index.ts - runtime selection
export const FileServiceLive = typeof window !== 'undefined' 
  ? createFileServiceWeb()
  : createFileServiceDesktop();

Common Use Cases

<summary>API Route Handler</summary> typescript export async function GET(request: Request) { const result = await userService.getUser(params.id); if (result.error) { switch (result.error.name) { case "UserNotFoundError": return new Response("Not found", { status: 404 }); case "DatabaseError": return new Response("Server error", { status: 500 }); } } return Response.json(result.data); }
<summary>Form Validation</summary> typescript function validateLoginForm(data: unknown): Result<LoginData, FormError> { const errors: Record<string, string[]> = {}; if (!isValidEmail(data?.email)) { errors.email = ["Invalid email format"]; } if (Object.keys(errors).length > 0) { return Err({ name: "FormError", message: "Validation failed", context: { fields: errors }, cause: undefined }); } return Ok(data as LoginData); }
<summary>React Hook</summary> typescript function useUser(id: number) { const [state, setState] = useState<{ loading: boolean; user?: User; error?: ApiError; }>({ loading: true }); useEffect(() => { fetchUser(id).then(result => { if (result.error) { setState({ loading: false, error: result.error }); } else { setState({ loading: false, user: result.data }); } }); }, [id]); return state; }

Comparison with Alternatives

wellcrafted fp-ts Effect neverthrow
Learning Curve Minimal Steep Steep Moderate
Syntax Native async/await Pipe operators Generators Method chains
Bundle Size < 2KB ~30KB ~50KB ~5KB
Type Safety ✅ Full ✅ Full ✅ Full ✅ Full
Serializable Errors ✅ Built-in ❌ Classes ❌ Classes ❌ Classes

API Reference

Result Functions

  • Ok(data) - Create success result
  • Err(error) - Create failure result
  • isOk(result) - Type guard for success
  • isErr(result) - Type guard for failure
  • trySync(options) - Wrap throwing function
  • tryAsync(options) - Wrap async function
  • partitionResults(results) - Split array into oks/errs

Error Functions

  • createTaggedError(name) - Creates error factory functions
    • Returns two functions: {ErrorName} and {ErrorName}Err
    • The first creates plain error objects
    • The second creates Err-wrapped errors

Types

  • Result<T, E> - Union of Ok<T> | Err<E>
  • TaggedError<T> - Structured error type
  • Brand<T, B> - Branded type wrapper

License

MIT


Made with ❤️ by developers who believe error handling should be delightful.