npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@symbion/runtype

v1.2.1

Published

Runtime type system for TypeScript

Readme

Runtime Type System for TypeScript

Description

RunType is a runtime type system for TypeScript.

It was inspired by IO-TS, but I made some opinionated changes in the concept. IO-TS is mathematically correct and follows JavaScript and TypeScript specifications to the letter. With RunType I wanted to create something more practical.

Some of the changes:

  • I am not too familiar with functional programming concepts, so I don't use them in RunType.
  • The struct combinator handles optional fields easier (without the partial + intersection things in IO-TS)
  • number decoder does not accept NaN.
  • Decoder accepts a config argument and supports type coercion and some other modifiers
  • Validators
  • Runtime type description generation (print() method)

Installation

npm install @symbion/runtype

Usage

Basic Usage

First create a type:

import T from '@symbion/runtype'

const tMyType = T.struct({
    s: T.string,
    n: T.optional(T.number)
})

You can extract a TypeScript type from it:

type MyType = T.TypeOf<typeof tMyType>

You can decode an unknown value:

const u: unknown = { s: 'string', n: 42 }

const decoded = T.decode(tMyType, u)
T.isOk(decoded)
// = true

const value: MyType = decoded.ok
// = { s: 'string', n: 42 }

Type Constructors

Primitive Types

| TypeScript | RunType | |------------|---------| | string | T.string | | number | T.number | | number (integer only) | T.integer | | number (integer alias) | T.id | | boolean | T.boolean | | bigint | T.bigint | | symbol | T.symbol | | Date | T.date |

Special Types

| TypeScript | RunType | |------------|---------| | undefined | T.undefinedValue | | null | T.nullValue | | true | T.trueValue | | false | T.falseValue | | any | T.any | | unknown | T.unknown | | {} (non-null object) | T.unknownObject | | void | T.voidType | | never | T.never |

Literal Types

T.literal('a', 'b', 3)
// TypeScript: 'a' | 'b' | 3

Compound Types

| Type | TypeScript | RunType | |------|------------|---------| | Array | Array<Type> | T.array(tType) | | Record | Record<string, Type> | T.record(tType) | | Struct | { s: string, n: number } | T.struct({ s: T.string, n: T.number }) | | Tuple | [string, number, Type] | T.tuple(T.string, T.number, tType) | | Union | string \| number \| Type | T.union(T.string, T.number, tType) | | Intersection | { s: string } & { n: number } | T.intersection(T.struct({ s: T.string }), T.struct({ n: T.number })) | | Tagged union | { tag: 's', s: string } \| { tag: 'n', n: number } | T.taggedUnion('tag')({ str: T.struct({ tag: T.literal('str'), s: T.string }), num: T.struct({ tag: T.literal('num'), n: T.number }) }) | | Key of | keyof { s: string, n: number } | T.keyof(T.struct({ s: T.string, n: T.number })) |

Wrapper Types

| Type | TypeScript | RunType | |------|------------|---------| | Optional | Type \| undefined | T.optional(tType) or tType.optional() | | Nullable | Type \| null \| undefined | T.nullable(tType) or tType.nullable() | | Default | Type (with fallback) | tType.default(value) or T.withDefault(tType, value) |

Chainable Methods

The optional(), nullable(), and default() modifiers can be chained directly on types:

// Chainable syntax
const tOptional = T.string.optional()
const tNullable = T.number.nullable()
const tWithDefault = T.string.default('fallback')

// Factory functions (also available)
const tOptional2 = T.optional(T.string)
const tNullable2 = T.nullable(T.number)
const tWithDefault2 = T.withDefault(T.string, 'fallback')

The default() modifier accepts either a value or a factory function:

T.string.default('static value')
T.array(T.string).default(() => [])  // Factory function for mutable defaults

Recursive Types

Recursive types can be created with T.lazy() and manual TypeScript types (because TypeScript can't infer recursive types):

interface Recursive {
    name: string
    children: Recursive[]
}

const tRecursive: T.Type<Recursive> = T.lazy(() => T.struct({
    name: T.string,
    children: T.array(tRecursive)
}))

Type Modifiers

partial

The T.partial() type modifier takes a Struct type and converts all fields to optional:

const tStruct = T.struct({
    s: T.string,
    n: T.optional(T.number)
})
// = { s: string, n?: number }

const tPartialType = T.partial(tStruct)
// = { s?: string, n?: number }

patch

The T.patch() type modifier takes a Struct type and converts all optional fields to nullable and all required fields to optional. It is useful for update APIs, where undefined or missing fields mean not to update and null value means to clear that field.

const tStruct = T.struct({
    s: T.string,
    n: T.optional(T.number)
})
// = { s: string, n?: number }

const tPatchType = T.patch(tStruct)
// = { s?: string, n?: number | null }

pick

The T.pick() type modifier takes a Struct type and picks the specified fields.

const tStruct = T.struct({
    s: T.string,
    n: T.optional(T.number),
    b: T.boolean
})
// = { s: string, n?: number, b: boolean }

const tPickType = T.pick(tStruct, ['s', 'n'])
// = { s: string, n?: number }

omit

The T.omit() type modifier takes a Struct type and omits the specified fields.

const tStruct = T.struct({
    s: T.string,
    n: T.optional(T.number),
    b: T.boolean
})
// = { s: string, n?: number, b: boolean }

const tOmitType = T.omit(tStruct, ['b'])
// = { s: string, n?: number }

deepPartial

The T.deepPartial() type modifier recursively makes all nested struct fields optional:

const tStruct = T.struct({
    name: T.string,
    address: T.struct({
        city: T.string,
        zip: T.string
    })
})
// = { name: string, address: { city: string, zip: string } }

const tDeepPartial = T.deepPartial(tStruct)
// = { name?: string, address?: { city?: string, zip?: string } }

Arrays and Date types are preserved as-is (not recursed into).

deepPatch

The T.deepPatch() type modifier is the deep version of patch(), applying patch semantics recursively:

const tStruct = T.struct({
    name: T.string,
    profile: T.struct({
        bio: T.optional(T.string),
        age: T.number
    })
})

const tDeepPatch = T.deepPatch(tStruct)
// Required fields become optional, optional fields become nullable, recursively

Decoder Options

The decode() function accepts an optional config argument. It can be used for type coercion:

T.decode(T.number, '42')
// = { _tag: 'Err', err: [ { path: [], error: 'expected number' } ] }

T.decode(T.number, '42', { coerceStringToNumber: true })
// = { _tag: 'Ok', ok: 42 }

Scalar Coercion

| Option | Description | |--------|-------------| | coerceNumberToString | Coerce numbers to string | | coerceNumberToBoolean | Coerce numbers to boolean | | coerceStringToNumber | Coerce string to number | | coerceScalar | Enable all scalar coercions above |

Date Coercion

| Option | Description | |--------|-------------| | coerceStringToDate | Coerce string to Date | | coerceNumberToDate | Coerce number to Date (timestamp) | | coerceDate | Enable all date coercions above |

BigInt Coercion

| Option | Description | |--------|-------------| | coerceStringToBigInt | Coerce string to bigint | | coerceNumberToBigInt | Coerce integer number to bigint | | coerceBigInt | Enable all bigint coercions above |

Array Coercion

| Option | Description | |--------|-------------| | coerceToArray | Custom function (value: unknown) => unknown to convert values to arrays |

Other Options

| Option | Type | Description | |--------|------|-------------| | coerceAll | boolean | Enable all coercion options | | acceptNaN | boolean | Make T.number accept NaN as a valid number | | unknownFields | 'reject' \| 'drop' \| 'discard' | How to handle unknown fields in structs: reject (error, default), drop (remove from output), discard (keep in output) |

Validation

The decode() function does type decoding, which is a synchronous function. RunType also handles data validation, which is defined as an asynchronous function. The type constructors define some validator methods and user defined functions can also be used.

const tMyType = T.struct({
    s: T.string.minLength(2)
})

Validation works like decoding:

await T.validate(T.string.minLength(2), 'abc')
// = { _tag: 'Ok', ok: 'abc' }

T.decode(T.string.minLength(2), 'a')
// = { _tag: 'Ok', ok: 'a' }

await T.validate(T.string.minLength(2), 'a')
// = { _tag: 'Err', err: [ { path: [], error: 'length must be at least 2' } ] }

String Validators

| Validator | Description | |-----------|-------------| | in(value1, value2, ...) | Value is one of the specified values | | length(len) | Length equals len | | length(min, max) | Length is between min and max | | minLength(len) | Length is at least len | | maxLength(len) | Length is at most len | | matches(pattern) | Value matches the RegExp pattern | | email() | Value is a valid email address |

Number Validators

| Validator | Description | |-----------|-------------| | in(value1, value2, ...) | Value is one of the specified values | | integer() | Value is an integer | | min(minValue) | Value is at least minValue | | max(maxValue) | Value is at most maxValue | | between(min, max) | Value is between min and max |

BigInt Validators

| Validator | Description | |-----------|-------------| | min(minValue) | Value is at least minValue | | max(maxValue) | Value is at most maxValue | | between(min, max) | Value is between min and max | | positive() | Value is greater than 0 | | negative() | Value is less than 0 | | nonNegative() | Value is 0 or greater |

Boolean Validators

| Validator | Description | |-----------|-------------| | true() | Value is true | | false() | Value is false |

Array Validators

| Validator | Description | |-----------|-------------| | length(len) | Length equals len | | length(min, max) | Length is between min and max | | minLength(len) | Length is at least len | | maxLength(len) | Length is at most len |

Literal Validators

| Validator | Description | |-----------|-------------| | in(value1, value2, ...) | Value is one of the specified values |

Custom Validators

function max42(v: number | undefined) {
    return (v || 0) <= 42 ? T.ok(v) : T.error("Max 42 is allowed!")
}

await T.validate(T.number.addValidator(max42), 43)
// = { _tag: 'Err', err: [ { path: [], error: "Max 42 is allowed!" } ] }

Internals

Missing Properties vs Undefined

TypeScript (because of JavaScript) differentiates missing properties and properties with undefined value. This is sometimes useful, however it makes it more difficult to handle this in runtime type systems. Take the following simple TypeScript type:

interface Person {
    name: string
    age?: number
}

In IO-TS you can create it like this:

const tPerson = T.intersection([
    T.type({
        name: T.string
    }),
    T.partial({
        age: T.number
    })
])
type Person = T.TypeOf<typeof tPerson>

RunType uses complex TypeScript mechanisms to achieve a simpler and readable syntax:

const tPerson = T.struct({
    name: T.string,
    age: T.optional(T.number)
})
type Person = T.TypeOf<typeof tPerson>

Under the hood RunType generates the same intersection type because of limitations in TypeScript, but it works the same as the original type:

type Person = { name: string } & { age?: number }

Closing Thoughts

If you want to boost your TypeScript knowledge to the next level I highly recommend to write a runtime type system. I guarantee it will be fun! :)