Skip to content

hoeck/simple-runtypes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version unit-tests npm-publish

Preface

I said I want SIMPLE runtypes. Just functions that validate and return data. Combine them into complex types and TypeScript knows their structure. That's how runtypes work.

Install

# npm npm install simple-runtypes # yarn yarn add simple-runtypes

Example

  1. Define the Runtype:
import * as st from 'simple-runtypes' const userRuntype = st.record({ id: st.integer(), name: st.string(), email: st.optional(st.string()), })

now, ReturnType<typeof userRuntype> is equivalent to

interface { id: number name: string email?: string }
  1. Use the runtype to validate untrusted data
userRuntype({ id: 1, name: 'matt' }) // => {id: 1, name: 'matt'} userRuntype({ id: 1, name: 'matt', isAdmin: true }) // throws an st.RuntypeError: "invalid field 'isAdmin' in data"

Invoke a runtype with use to get a plain value back instead of throwing errors:

st.use(userRuntype, { id: 1, name: 'matt' }) // => {ok: true, result: {id: 1, name: 'matt'}} st.use(userRuntype, { id: 1, name: 'matt', isAdmin: true }) // => {ok: false, error: FAIL} st.getFormattedError(FAIL) // => 'invalid keys in record: ["isAdmin"] at `<value>` in `{"id":1,"name": "matt", ... }`'

Not throwing errors is way more efficient and less obscure.

Throwing errors and catching them outside is more convenient:

try { ... // code that uses runtypes } catch (e) { if (st.isRuntypeError(e)) { console.error(getFormattedError(e)) return } throw e }

Why?

Why should I use this over the plethora of other runtype validation libraries available?

  1. Strict: by default safe against __proto__ injection attacks and unwanted properties
  2. Fast: check the benchmark
  3. Friendly: no use of eval, and a small footprint with no dependencies
  4. Flexible: optionally modify the data while it's being checked - trim strings, convert numbers, parse dates

Benchmarks

@moltar has done a great job comparing existing runtime type-checking libraries in moltar/typescript-runtime-type-benchmarks.

@pongo has benchmarked simple-runtypes against io-ts in pongo/benchmark-simple-runtypes.

Documentation

Intro

A Runtype is a function that:

  1. receives an unknown value
  2. returns that value or a copy if all validations pass
  3. throws a RuntypeError when validation fails or returns ValidationResult when passed to use
interface Runtype<T> { (v: unknown) => T }

Runtypes are constructed by calling factory functions. For instance, string creates and returns a string runtype. Check the factory functions documentation for more details.

Usage Examples

Nesting

Collection runtypes such as record, array, and tuple take runtypes as their parameters:

const nestedRuntype = st.record({ name: st.string(), items: st.array(st.record({ id: st.integer, label: st.string() })), }) nestedRuntype({ name: 'foo', items: [{ id: 3, label: 'bar' }], }) // => returns the same data

Strict Property Checks

When using record, any properties which are not defined in the runtype will cause the runtype to fail:

const strict = st.record({ name: st.string() }) strict({ name: 'foo', other: 123 }) // => RuntypeError: Unknown attribute 'other'

Using record will keep you safe from any __proto__ injection or overriding attempts.

Ignore Individual Properties

To ignore individual properties, use ignore, unknown or any:

const strict = st.record({ name: st.string(), other: st.ignore() }) strict({ name: 'foo', other: 123 }) // => {name: foo, other: undefined}

Optional Properties

Use the optional runtype to create optional properties:

const strict = st.record({ color: st.optional(st.string()), width: st.optional(st.number()), })

Non-strict Property Checks

Use nonStrict to only validate known properties and remove everything else:

const nonStrictRecord = st.nonStrict(st.record({ name: st.string() })) nonStrictRecord({ name: 'foo', other: 123, bar: [] }) // => {name: foo}

Discriminating Unions

simple-runtypes supports Discriminating Unions via the union runtype.

The example found in the TypeScript Handbook translated to simple-runtypes:

const networkLoadingState = st.record({ state: st.literal('loading'), }) const networkFailedState = st.record({ state: st.literal('failed'), code: st.number(), }) const networkSuccessState = st.record({ state: st.literal('success'), response: st.record({ title: st.string(), duration: st.number(), summary: st.string(), }), }) const networdStateRuntype = st.union( networkLoadingState, networkFailedState, networkSuccessState, ) type NetworkState = ReturnType<typeof networkStateRuntype>

Finding the runtype to validate a specific discriminating union with is done efficiently with a Map.

Custom Runtypes

Write your own runtypes as plain functions, e.g. if you want to turn a string into a BigInt:

const bigIntStringRuntype = st.string({ match: /^-?[0-9]+n$/ }) const bigIntRuntype = st.runtype((v) => { const stringCheck = st.use(bigIntStringRuntype, v) if (!stringCheck.ok) { return stringCheck.error } return BigInt(stringCheck.result.slice(0, -1)) }) bigIntRuntype('123n') // => 123n bigIntRuntype('2.2') // => error: "expected string to match ..."

Reference

Basic runtypes that match JavaScript/TypeScript types:

Meta runtypes:

Objects and Array Runtypes:

Combinators:

Shortcuts:

Roadmap / Todos

  • size - a meta-runtype that imposes a size limit on types, maybe via convert-to-json and .length on the value passed to it
  • rename stringLiteralUnion to literals or literalUnion and make it work on all types that literal accepts
  • rename record to object: #69
  • improve docs:
    • preface: what is a runtype and why is it useful
    • why: explain or link to example that shows "strict by default"
    • show that simple-runtypes is feature complete because it can
      1. express all TypeScript types
      2. is extendable with custom runtypes (add documentation)
    • add small frontend and backend example projects that show how to use simple-runtypes in production
  • test all types with tsd
  • add more combinators: partial, required, get, ...
  • separate Runtype and InternalRuntype and type runtype internals (see this comment)

current tasks (metadata) notes

  • check that intersection & union tests do properly test the distribution stuff
  • make getMetadata public
  • maybe make metadata typed and include all options so that you can walk the tree to create testdata orjson-schemas from types
  • maybe add a serialize function to each runtype too? to use instead of JSON.stringify and to provide a full-service library?
  • maybe make any a forbidden type of a runtype

About

Small, efficient and extendable runtype library for Typescript

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 6