npm package: @metreeca/type
Description: A lightweight TypeScript library for composable runtime type validation.
Author: Metreeca
Latest version: 0.1.2

# @metreeca/type

[![npm](https://img.shields.io/npm/v/@metreeca/type)](https://www.npmjs.com/package/@metreeca/type)

A lightweight TypeScript library for composable runtime type validation.

**@metreeca/type** provides an idiomatic, easy-to-use functional API for validating unknown data against type
definitions using guards. Guards validate type structure (for instance, that `radius` is a number) while leaving
semantic constraints (for instance, that `radius >= 0`) to application logic. Key features include:

- **Structural validation** › Focus on type structure for clean separation of concerns
- **Type safety** › Seamless type inference and automatic type narrowing
- **Composable guards** › Build complex validators from simple primitives
- **Union/intersection support** › Handle discriminated unions and type intersections
- **Recursive types** › Define self-referencing and mutually recursive structures

# Installation

```shell
npm install @metreeca/type
```

> [!WARNING]
>
> TypeScript consumers must use `"moduleResolution": "nodenext"/"node16"/"bundler"` in `tsconfig.json`.
> The legacy `"node"` resolver is not supported.

# Usage

> [!NOTE]
>
> This section introduces essential concepts and common patterns: see the
> [API reference](https://metreeca.github.io/type/modules.html) for complete coverage.

A **guard** is a function that takes an `unknown` value and returns an empty string on success or an error message on
failure. Guards perform **structural validation** only: they validate that a value has the expected type structure, not
semantic constraints like `radius >= 0`.

**@metreeca/type** provides three main abstractions built around guards:

- **[Guards](https://metreeca.github.io/type/modules.html#Guards)**: Functions that validate values against
  primitive types, arrays, and objects
- **[Assertions](https://metreeca.github.io/type/modules.html#Assertions)**: Functions that apply guards to produce
  type predicates and validating casts
- **[Combinators](https://metreeca.github.io/type/modules.html#Combinators)**: Functions that combine multiple guards

## Defining Guards

Define guards as functions returning a `Trace`.

```typescript
import { anObject, aString, aNumber, type Trace } from "@metreeca/type";

interface User {
    readonly name: string;
    readonly age: number;
}

function aUser(value: unknown): Trace<User> {
    return anObject(value, {
        name: aString,
        age: aNumber
    });
}
```

## Using Assertions

Use `is()` for conditional narrowing, or `as()` for validating casts.

```typescript
import { is, as } from "@metreeca/type";

function processUser(value: unknown): string {
    if (is(aUser, value)) {
        return value.name;  // type narrowed to User
    } else {
        return "unknown";
    }
}

function castUser(value: unknown): User {
    return as(aUser, value);  // throws TypeError on invalid input
}
```

> [!CAUTION]
>
> **Circular references are not supported**. Validating objects with cycles causes stack overflow.

## Primitives

Guards for primitive types cover common JavaScript value types.

```typescript
import { anObject, aNull, aBoolean, aNumber, aString, aFunction, type Trace } from "@metreeca/type";

interface Profile {
    readonly avatar: null;
    readonly verified: boolean;
    readonly age: number;
    readonly name: string;
    readonly onClick: Function;
}

function aProfile(value: unknown): Trace<Profile> {
    return anObject(value, {
        avatar: aNull,
        verified: aBoolean,
        age: aNumber,
        name: aString,
        onClick: aFunction
    });
}
```

## Arrays

Use `anArray()` without an element guard to accept any array, or with an element guard to validate each item.

```typescript
import { anObject, anArray, aNumber, type Trace } from "@metreeca/type";

interface Playlist {
    readonly tracks: readonly unknown[];
    readonly ratings: readonly number[];
}

function aPlaylist(value: unknown): Trace<Playlist> {
    return anObject(value, {
        tracks: anArray,                      // any array
        ratings: v => anArray(v, aNumber)     // array of numbers
    });
}
```

## Objects

Use `anObject()` without a schema to accept any plain object.

```typescript
import { anObject, type Trace } from "@metreeca/type";

interface Headers {
    readonly [key: string]: unknown;
}

function aHeaders(value: unknown): Trace<Headers> {
    return anObject(value);  // any plain object
}
```

Add key and value guards for uniform validation of all entries.

```typescript
import { anObject, aString, type Trace } from "@metreeca/type";
import { isTag, type Tag } from "./tags";

interface Dictionary {
    readonly [tag: Tag]: string;
}

function aDictionary(value: unknown): Trace<Dictionary> {
    return anObject(value,
        (v: unknown) => isTag(v) ? "" : `malformed tag <${v}>`,
        aString
    );
}
```

> [!NOTE]
> Validating `Tag` with a regex is structural type validation: the pattern defines what a `Tag` is, not a semantic
> constraint on strings.

Add a schema to create a **closed** object: only schema properties are allowed, extra properties are rejected.

```typescript
import { anObject, aString, aNumber, type Trace } from "@metreeca/type";

interface Package {
    readonly name: string;
    readonly version: number;
}

function aPackage(value: unknown): Trace<Package> {
    return anObject(value, {
        name: aString,
        version: aNumber
    });  // closed: extra properties rejected
}
```

Add a rest guard to create an **open** object: extra properties are validated by the rest guard.

```typescript
import { anObject, aString, aNumber, anUnknown, type Trace } from "@metreeca/type";

interface Options {
    readonly name: string;
    readonly version: number;
    readonly [key: string]: unknown;
}

function anOptions(value: unknown): Trace<Options> {
    return anObject(value, {
        name: aString,
        version: aNumber
    }, anUnknown);  // open: extra properties allowed
}
```

## Unions

Use `aUnion()` to validate a value against guards disjunctively. Validation succeeds if any guard passes. Use literal
values in schemas to create discriminated unions.

```typescript
import { aUnion, anObject, aString, type Trace } from "@metreeca/type";

type Result = Success | Failure;

interface Success {
    readonly ok: true;
    readonly value: string;
}

interface Failure {
    readonly ok: false;
    readonly error: string;
}

function aResult(value: unknown): Trace<Result> {
    return aUnion(value, {
        aSuccess,
        aFailure
    });
}

function aSuccess(value: unknown): Trace<Success> {
    return anObject(value, {
        ok: true,       // literal value match
        value: aString
    });
}

function aFailure(value: unknown): Trace<Failure> {
    return anObject(value, {
        ok: false,
        error: aString
    });
}
```

## Intersections

Use `anIntersection()` to validate a value against multiple guards conjunctively.
All guards must pass for validation to succeed.

```typescript
import { anIntersection, anObject, aString, aBoolean, type Trace } from "@metreeca/type";

interface Identifiable {
    readonly id: string;
}

interface Trackable {
    readonly active: boolean;
}

type Device = Identifiable & Trackable;

function aDevice(value: unknown): Trace<Device> {
    return anIntersection(value, [
        v => anObject(v, { id: aString }),
        v => anObject(v, { active: aBoolean })
    ]);
}
```

## Recursion

Define self-referencing types by wrapping guard calls in lambdas.

```typescript
import { anObject, anArray, aNumber, type Trace } from "@metreeca/type";

interface Node {
    readonly value: number;
    readonly children: readonly Node[];
}

function aNode(value: unknown): Trace<Node> {
    return anObject(value, {
        value: aNumber,
        children: v => anArray(v, aNode)  // self-reference via lambda
    });
}
```

## Putting It All Together

A realistic example combining multiple patterns from previous sections.

```typescript
import { aUnion, anObject, anArray, aNumber, type Trace } from "@metreeca/type";

type Shape =
    | Circle
    | Rectangle
    | Composite;

interface Circle {
    readonly type: "circle";
    readonly x: number;
    readonly y: number;
    readonly radius: number;
}

interface Rectangle {
    readonly type: "rectangle";
    readonly x: number;
    readonly y: number;
    readonly width: number;
    readonly height: number;
}

interface Composite {
    readonly type: "composite";
    readonly shapes: readonly Shape[];
}

function aShape(value: unknown): Trace<Shape> {
    return aUnion(value, {
        aCircle,
        aRectangle,
        aComposite
    });
}

function aCircle(value: unknown): Trace<Circle> {
    return anObject(value, {
        type: "circle",
        x: aNumber,
        y: aNumber,
        radius: aNumber
    });
}

function aRectangle(value: unknown): Trace<Rectangle> {
    return anObject(value, {
        type: "rectangle",
        x: aNumber,
        y: aNumber,
        width: aNumber,
        height: aNumber
    });
}

function aComposite(value: unknown): Trace<Composite> {
    return anObject(value, {
        type: "composite",
        shapes: v => anArray(v, aShape)  // recursive reference
    });
}
```

# Support

- Open an [issue](https://github.com/metreeca/type/issues) to report a problem or to suggest a new feature
- Start a [discussion](https://github.com/metreeca/type/discussions) to ask a how-to question or to share an idea

# License

This project is licensed under the Apache 2.0 License –
see [LICENSE](https://github.com/metreeca/type?tab=Apache-2.0-1-ov-file) file for details.