Skip to content

Small, efficient and extendable runtype library for Typescript

License

Notifications You must be signed in to change notification settings

hoeck/simple-runtypes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

99e6bb9 · Dec 19, 2023
Dec 19, 2023
Sep 13, 2020
Dec 18, 2023
Aug 7, 2022
Dec 19, 2023
Aug 3, 2020
Sep 11, 2019
Dec 18, 2023
Dec 17, 2022
Dec 18, 2023
Dec 19, 2023
Apr 18, 2021
Dec 18, 2023

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