@metreeca/type
v0.1.2
Published
A lightweight TypeScript library for composable runtime type validation.
Readme
@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
npm install @metreeca/type[!WARNING]
TypeScript consumers must use
"moduleResolution": "nodenext"/"node16"/"bundler"intsconfig.json. The legacy"node"resolver is not supported.
Usage
[!NOTE]
This section introduces essential concepts and common patterns: see the API reference 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: Functions that validate values against primitive types, arrays, and objects
- Assertions: Functions that apply guards to produce type predicates and validating casts
- Combinators: Functions that combine multiple guards
Defining Guards
Define guards as functions returning a Trace.
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.
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.
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.
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.
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.
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
Tagwith a regex is structural type validation: the pattern defines what aTagis, not a semantic constraint on strings.
Add a schema to create a closed object: only schema properties are allowed, extra properties are rejected.
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.
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.
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.
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.
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.
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 to report a problem or to suggest a new feature
- Start a discussion to ask a how-to question or to share an idea
License
This project is licensed under the Apache 2.0 License – see LICENSE file for details.
