An open API service indexing awesome lists of open source software.

https://github.com/0xdsqr/ts-match

Type-safe pattern matching for TypeScript with exhaustiveness checking and zero dependencies
https://github.com/0xdsqr/ts-match

bun functional-programming pattern-matching typescript

Last synced: 2 months ago
JSON representation

Type-safe pattern matching for TypeScript with exhaustiveness checking and zero dependencies

Awesome Lists containing this project

README

          


ts-match - Type-safe pattern matching for TypeScript


Tests Status
TypeScript
Code Coverage


A lightweight, type-safe pattern matching utility for TypeScript. Transform messy `if/else` chains and verbose `switch` statements into clean, functional expressions with full type safety and exhaustiveness checking.

## ⇁ The Problem
You're tired of verbose `switch` statements and nested `if/else` chains. You want type-safe conditional logic that catches missing cases at compile time and returns values directly without manual breaks or forgotten returns.

## ⇁ The Solution
Pattern matching is a powerful control flow construct that's cleaner and more expressive than traditional approaches. With `ts-match`, you get compile-time type safety, exhaustiveness checking, and a functional approach to conditional logic.

## ⇁ Key Features

- 🎯 **Type-safe** - Full TypeScript inference, no `any` types
- 🛡️ **Exhaustive** - Compiler catches missing cases
- 🚀 **Zero dependencies** - <1KB bundle size
- ✨ **Expression-based** - Returns values instead of requiring mutations
- 📦 **Tree-shakeable** - Import only what you need

## ⇁ Installation

| Package Manager | Command |
| --------------- | ---------------------------- |
| **npm** | `npm install @dsqr/ts-match` |
| **pnpm** | `pnpm add @dsqr/ts-match` |
| **bun** | `bun add @dsqr/ts-match` |
| **yarn** | `yarn add @dsqr/ts-match` |

## ⇁ Quick Start

```typescript
import { match } from "@dsqr/ts-match"

// Handle user permissions
const getAccessLevel = (role: "admin" | "user" | "guest") =>
match(role)({
admin: () => ({ canDelete: true, canEdit: true, canView: true }),
user: () => ({ canDelete: false, canEdit: true, canView: true }),
guest: () => ({ canDelete: false, canEdit: false, canView: true }),
})

console.log(getAccessLevel("user"))
// { canDelete: false, canEdit: true, canView: true }
```

## ⇁ Development with Nix

### Development Environment Setup

Clone and enter the development environment:

```bash
git clone https://github.com/0xdsqr/ts-match.git
cd ts-match
nix develop
```

### Testing

Run tests with coverage:

```bash
# Run tests directly
bun test --coverage

# Run tests via Nix (with visible output)
nix build .#test

# Run tests via Nix (silent, for CI)
nix flake check
```

### Running Examples

```bash
# Run examples directly
bun run examples/index.ts

# Run examples via Nix
nix run .#examples
```

## ⇁ Real-World Comparison

Here's how `ts-match` compares to traditional approaches:

### HTTP Status Handling

```typescript
// Traditional switch - verbose, imperative
function handleStatus(code: number): string {
switch (code) {
case 200:
return "Success"
case 404:
return "Not Found"
case 500:
return "Server Error"
default:
return "Unknown"
}
}

// ts-match - clean, functional, type-safe
const handleStatus = (code: number) =>
match(code)({
200: () => "Success",
404: () => "Not Found",
500: () => "Server Error",
_: () => "Unknown",
})
```

### React State Management

```typescript
type LoadingState = "idle" | "loading" | "success" | "error";

// Traditional - breaks JSX flow
function StatusComponent({ state }: { state: LoadingState }) {
let content;
switch (state) {
case "idle": content =

Ready to start
; break;
case "loading": content = ; break;
case "success": content = ; break;
case "error": content = ; break;
}
return
{content}
;
}

// ts-match - stays in expression context
function StatusComponent({ state }: { state: LoadingState }) {
return (


{match(state)({
"idle": () =>
Ready to start
,
"loading": () => ,
"success": () => ,
"error": () =>
})}

);
}
```

## ⇁ API Reference

Basic Pattern Matching

Match against string, number, or symbol values with type-safe handlers.

**String matching**

```typescript
const handleHttpMethod = (method: "GET" | "POST" | "PUT" | "DELETE") =>
match(method)({
GET: () => "Fetching data",
POST: () => "Creating resource",
PUT: () => "Updating resource",
DELETE: () => "Removing resource",
})
```

**Number matching**

```typescript
const getHttpMessage = (code: number) =>
match(code)({
200: () => "OK",
201: () => "Created",
400: () => "Bad Request",
404: () => "Not Found",
500: () => "Server Error",
_: () => "Unknown status code",
})
```

**Symbol matching**

```typescript
const PENDING = Symbol("pending")
const COMPLETE = Symbol("complete")

const getTaskStatus = (status: symbol) =>
match(status)({
[PENDING]: () => "Task in progress",
[COMPLETE]: () => "Task finished",
_: () => "Unknown status",
})
```

Default Cases and Error Handling

Handle pattern matching failures gracefully with the custom `MatchError` class.

**With default fallback**

```typescript
const handleUserInput = (input: string) =>
match(input)({
yes: () => true,
no: () => false,
_: () => null, // Fallback for any other input
})
```

**Without default (throws MatchError)**

```typescript
import { MatchError } from "@dsqr/ts-match"

try {
const result = match("maybe")({
yes: () => true,
no: () => false,
// No "_" - will throw for unmatched values
})
} catch (error) {
if (error instanceof MatchError) {
console.log("Unhandled input:", error.message)
}
}
```

Advanced Patterns

**Nested matching**

```typescript
const authorize = (role: string, action: string) =>
match(role)({
admin: () => "allowed", // Admins can do anything
user: () =>
match(action)({
read: () => "allowed",
write: () => "allowed",
_: () => "denied",
}),
guest: () =>
match(action)({
read: () => "allowed",
_: () => "denied",
}),
_: () => "invalid role",
})
```

**Complex return types**

```typescript
interface DatabaseConfig {
host: string
port: number
ssl: boolean
}

const getDatabaseConfig = (env: string): DatabaseConfig =>
match(env)({
production: () => ({
host: "prod-db.company.com",
port: 5432,
ssl: true,
}),
development: () => ({
host: "localhost",
port: 5432,
ssl: false,
}),
_: () => ({
host: "localhost",
port: 5432,
ssl: false,
}),
})
```

Type Helpers

Use exported types for better code organization and reusability.

**Reusable patterns**

```typescript
import { Pattern } from "@dsqr/ts-match"

type Theme = "light" | "dark" | "auto"

// Define reusable pattern object
const themeStyles: Pattern = {
light: () => "bg-white text-black",
dark: () => "bg-black text-white",
auto: () => "bg-gray-100 text-gray-900",
}

// Use anywhere
const getThemeClasses = (theme: Theme) => match(theme)(themeStyles)
```

**Matcher - Higher-order pattern functions**

```typescript
import { Matcher, Pattern } from "@dsqr/ts-match"

class HttpService {
constructor(private matcher: Matcher<"GET" | "POST">) {}

request(method: "GET" | "POST") {
return this.matcher({
GET: () => "Fetching data...",
POST: () => "Sending data...",
_: () => "Unsupported method",
})
}
}
```

## ⇁ Real-World Examples

Form Validation

```typescript
type ValidationResult = "valid" | "empty" | "too_short" | "invalid_email"

const getValidationMessage = (result: ValidationResult) =>
match(result)({
valid: () => null,
empty: () => "This field is required",
too_short: () => "Must be at least 3 characters",
invalid_email: () => "Please enter a valid email address",
})
```

File Processing

```typescript
const processFile = (extension: string) =>
match(extension.toLowerCase())({
jpg: () => processImage,
png: () => processImage,
pdf: () => processPDF,
txt: () => processText,
csv: () => processCSV,
_: () => () => {
throw new Error(`Unsupported file type: ${extension}`)
},
})
```

Game State Management

```typescript
type GameState = "menu" | "playing" | "paused" | "game_over";

const renderGameScreen = (state: GameState) =>
match(state)({
"menu": () => ,
"playing": () => ,
"paused": () => ,
"game_over": () =>
});
```

API Response Handling

```typescript
interface ApiResponse {
status: "success" | "error" | "loading"
data?: T
error?: string
}

const processResponse = (response: ApiResponse) =>
match(response.status)({
success: () => ({
ui: "Success!",
data: response.data,
error: null,
}),
error: () => ({
ui: "Error occurred",
data: null,
error: response.error,
}),
loading: () => ({
ui: "Loading...",
data: null,
error: null,
}),
})
```

State Management

```typescript
type AppState = "loading" | "ready" | "error"

const renderApp = (state: AppState) =>
match(state)({
loading: () => `

Loading...
`,
ready: () => `
App ready!
`,
error: () => `
Something went wrong
`,
})
```

Route Handling

```typescript
const handleRoute = (path: string) =>
match(path)({
"/": () => renderHomePage(),
"/about": () => renderAboutPage(),
"/contact": () => renderContactPage(),
_: () => render404Page(),
})
```

## ⇁ TypeScript Features

ts-match leverages TypeScript's type system for safety and developer experience:

**Exhaustiveness checking**

```typescript
type Status = "pending" | "approved" | "rejected"

// This compiles
const handleComplete = (status: Status) =>
match(status)({
pending: () => "Waiting",
approved: () => "Done",
rejected: () => "Failed",
})

// TypeScript error - missing "rejected" case
const handleIncomplete = (status: Status) =>
match(status)({
pending: () => "Waiting",
approved: () => "Done",
// Error: missing pattern for "rejected"
})
```

**Full type inference**

```typescript
// Return type automatically inferred as string | number
const getValue = (key: "name" | "age") =>
match(key)({
name: () => "John", // string
age: () => 25, // number
})
```

## ⇁ Contributing

Built for learning and experimentation. Open a PR or issue if you want, but no promises - this is a simple two-file project. Feel free to fork it and make it your own!

## ⇁ License

MIT - Do whatever you want with it.

---

*Built for developers with ADHD by developers with ADHD.*

*Your experiments deserve a home.* 🏠