npm package: @moojo/misc-constructs
Description: Compact TypeScript utilities that improve type-safety for error handling and exhaustiveness checking
Author: imaman
Latest version: 0.10.0

# @moojo/misc-constructs

[![npm version](https://img.shields.io/npm/v/@moojo/misc-constructs.svg)](https://www.npmjs.com/package/@moojo/misc-constructs)
[![CI](https://github.com/moojo-tech/moojo-oss/actions/workflows/main.yml/badge.svg)](https://github.com/moojo-tech/moojo-oss/actions/workflows/main.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

TypeScript utilities for type-safe error handling and exhaustiveness checking.

## Installation

```bash
npm install @moojo/misc-constructs
```

## API

### Exhaustive Handling of String Unions

#### shouldNeverHappen

Compile-time exhaustiveness checking for union types. Place at the end of conditional chains to ensure all cases are handled.

```typescript
import { shouldNeverHappen } from '@moojo/misc-constructs'

type Status = 'pending' | 'success' | 'failure'

function handleStatus(status: Status) {
  if (status === 'pending') return 'Waiting...'
  if (status === 'success') return 'Done!'
  if (status === 'failure') return 'Failed'
  shouldNeverHappen(status) // Compile error if a case is missing
}
```

When you add a new variant to a union type, the compiler won't warn you about all the switch statements and if-chains that need updating. Code silently falls through, and bugs appear at runtime. By placing `shouldNeverHappen(x)` at the end of your conditional logic, TypeScript will produce a compile error whenever a case is unhandled. This turns a runtime bug into a build-time error, making union type refactoring safe.

#### switchOn

Type-safe pattern matching. Compiler enforces that all cases are present and no extra cases exist.

```typescript
import { switchOn } from '@moojo/misc-constructs'

type Op = '+' | '-' | '*'

function calculate(op: Op, a: number, b: number): number {
  return switchOn(op, {
    '+': () => a + b,
    '-': () => a - b,
    '*': () => a * b,
  })
}
```

TypeScript's `switch` statement doesn't enforce exhaustiveness by default, and it's a statement rather than an expression, so you can't use it inline. `switchOn` solves both problems: it's an expression that returns a value, and the compiler will reject code that has missing or extraneous cases. This makes it impossible to forget a case when the union type changes, and impossible to leave dead code behind when a case is removed.

### Throwing Errors

#### failMe

Throws an error when evaluated. Useful as the right-hand side of `??` or `||` when a fallback value cannot be provided.

```typescript
import { failMe } from '@moojo/misc-constructs'

// Environment variables
const port = process.env['PORT'] || failMe('PORT env variable is required')

// Nullish coalescing
const config = loadConfig() ?? failMe('config must be defined')

// After .find() - when the item must exist
const user = users.find(u => u.id === id) ?? failMe(`user not found: ${id}`)

// After .match() - when the pattern must match
const [_, commit] = input.match(/^@([0-9a-f]+)$/) ?? failMe(`bad commit hash: ${input}`)

// Ternary chains - when all branches must be covered
const handler = isAdmin ? adminHandler : isGuest ? guestHandler : failMe('unknown user type')
```

Compare with the traditional approach using `throw`:

```typescript
// With failMe - concise, expression-based
const port = process.env['PORT'] || failMe('PORT is required')

// Without failMe - verbose, statement-based
const port = process.env['PORT']
if (!port) {
  throw new Error('PORT is required')
}

// With failMe - stays in expression context
const user = users.find(u => u.id === id) ?? failMe(`not found: ${id}`)

// Without failMe - breaks the flow
const user = users.find(u => u.id === id)
if (!user) {
  throw new Error(`not found: ${id}`)
}
```

`failMe` is a natural fit for teams that follow the fail-early-and-loud philosophy. When a precondition is violated, you want the code to fail immediately with a clear message, not silently continue with `undefined` or a default value that masks the real problem. `failMe` makes these assertions visible and keeps them inline with the code that depends on them.

### Handling Errors

#### errorLike

Safely extracts error properties from caught values. Avoids unsafe type assertions on `unknown`.

```typescript
import { errorLike } from '@moojo/misc-constructs'

try {
  await riskyOperation()
} catch (e) {
  // Without fallback - message may be undefined
  const { message, stack } = errorLike(e)

  // With fallback - message is guaranteed to be a string
  const err = errorLike(e, 'Operation failed')
  console.error(err.message) // string, never undefined
}
```

In JavaScript, anything can be thrown—not just `Error` instances, but strings, numbers, objects, or even `undefined`. TypeScript correctly types caught values as `unknown`, but this makes accessing `.message` or `.stack` awkward. The common workaround is `(e as Error).message`, but this is unsafe: if someone threw a string or a plain object, your code will misbehave. `errorLike` inspects the caught value at runtime and extracts properties only if they exist and are strings, giving you a consistent interface without lying to the type system.

#### catchMe / catchMeAsync

Captures execution results or errors in a structured format. Primarily for testing scenarios where you need to examine error properties.

```typescript
import { catchMe, catchMeAsync } from '@moojo/misc-constructs'

// Synchronous
const result = catchMe(() => JSON.parse('{invalid}'))
if (!result.success) {
  console.log(result.error)      // The thrown value
  console.log(result.postmortem) // All enumerable properties of the error
}

// Asynchronous
const asyncResult = await catchMeAsync(async () => {
  return await fetchData()
})
if (asyncResult.success) {
  console.log(asyncResult.data)
}
```

In tests:

```typescript
test('throws on invalid input', () => {
  expect(catchMe(() => parseConfig(''))).toMatchObject({
    success: false,
    postmortem: {
      message: expect.stringContaining('empty input'),
    },
  })
})
```

Jest's `toThrow()` matcher only lets you check the error message or type. But errors often carry additional properties—error codes, HTTP status codes, metadata—that you want to verify. `catchMe` captures the thrown value and extracts all its enumerable properties into a `postmortem` object, which you can then assert against with `toMatchObject`. This gives you fine-grained control over error assertions without resorting to try-catch blocks in every test.

## License

MIT