Skip to content

Latest commit

 

History

History
333 lines (243 loc) · 10.2 KB

README.md

File metadata and controls

333 lines (243 loc) · 10.2 KB

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