wellcrafted
Version:
Delightful TypeScript patterns for elegant, type-safe applications
490 lines (388 loc) • 12.1 kB
Markdown
# wellcrafted
[](https://www.npmjs.com/package/wellcrafted)
[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
[](https://bundlephobia.com/package/wellcrafted)
*Delightful TypeScript utilities for elegant, type-safe applications*
## Transform unpredictable errors into type-safe results
```typescript
// ❌ 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
```typescript
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
```typescript
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
```typescript
import { createTaggedError } from "wellcrafted/error";
const { ApiError, ApiErr } = createTaggedError("ApiError");
// ApiError() creates error object, ApiErr() creates Err-wrapped error
```
## Installation
```bash
npm install wellcrafted
```
## Quick Start
```typescript
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
<table>
<tr>
<td>
**🎯 Explicit Error Handling**
All errors visible in function signatures
</td>
<td>
**📦 Serialization-Safe**
Plain objects work everywhere
</td>
<td>
**✨ Elegant API**
Clean, intuitive patterns
</td>
</tr>
<tr>
<td>
**🔍 Zero Magic**
~50 lines of core code
</td>
<td>
**🚀 Lightweight**
Zero dependencies, < 2KB
</td>
<td>
**🎨 Composable**
Mix and match utilities
</td>
</tr>
</table>
## The Result Pattern Explained
The Result type makes error handling explicit and type-safe:
```typescript
// 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:
```typescript
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
```typescript
const { data, error } = await someOperation();
if (error) {
// Handle error with full type safety
return;
}
// Use data - TypeScript knows it's safe
```
### Wrap Unsafe Operations
```typescript
// 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
```typescript
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
```typescript
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:
```typescript
// 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
<details>
<summary><b>API Route Handler</b></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);
}
```
</details>
<details>
<summary><b>Form Validation</b></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);
}
```
</details>
<details>
<summary><b>React Hook</b></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;
}
```
</details>
## 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
## Learn More
- 📖 [Full Documentation](https://github.com/your-repo/wellcrafted/wiki)
- 🚀 [Examples](https://github.com/your-repo/wellcrafted/tree/main/examples)
- 💬 [Discussions](https://github.com/your-repo/wellcrafted/discussions)
## License
MIT
---
Made with ❤️ by developers who believe error handling should be delightful.