包详细信息

simple-runtypes

hoeck4.3kMIT7.1.3

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.

自述文件

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 install simple-runtypes or 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.

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, a small footprint and 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

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'

To ignore single 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}

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

const sloppy = st.sloppyRecord({name: st.string()})

sloppy({name: 'foo', other: 123, bar: []})
// => {name: foo}

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

Optional Properties

Use the optional runtype to create optional properties:

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

Nesting

Collection runtypes such as record, array, 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

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
  • nonStrict modifier instead of sloppy: #68
  • 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)

更新日志

7.1.3

  • fix: invalid key message was displaying the object instead of the keys, see #91

7.1.2

  • fix: json now keeps the type information from the passed runtype

7.1.1

  • fix: make sloppyRecord impure (closes #60)

7.1.0

  • add json runtype
  • add minLength option to string runtype

7.0.0

  • rename nullable to nullOr to be consistent with undefinedOr (because undefinable is not a good name)
  • add undefinedOr that works like the old optional type
  • real optional keys in records: Change optional to be only usable in record. Using it will result in the key to be infered as optional: record({key: optional(number())}) results in {key?: number} as the type (old behavior would infer {key: undefined | number}, you can get the old behaviour by using undefinedOr instead of optional)
  • add dictionary and remove numberIndex and stringIndex Use dictionary(stringAsInteger(), otherType) to replace numberIndex and dictionary(string(), otherType) to replace stringIndex.

6.3.0

  • add partial combinator runtype

6.2.0

  • add match option to string to test the string against a regular expression

6.1.1

  • fix and improve error value and path formatting (shout outs to @pabra for the patch)

6.1.0

  • support intersection of unions and records

6.0.1

  • fix: rename useRuntype to use

6.0.0

  • change useRuntype to return a ValidationResult to support using runtypes without relying on exceptions and win in benchmarks
  • rename fail to createError
  • make isFail private, use ValidationResult.ok instead

5.0.2

  • fix fail to not return any but Fail Now, custom runtypes will result in the correct inferred type of Runtype<T> instead of Runtype<any> for custom runtypes that might return a failure.

5.0.1

  • fix missing error utility (getFormattedError(), ...) exports

5.0.0

  • add useRuntype to call other runtypes in custom runtypes
  • rename enumValue to enum
  • remove discriminatedUnion - use the normal union instead. The discriminant key gets inferrred automatically by union now.

4.1.0

  • fix error messages
  • add sloppyRecord (props to @pabra)
  • add undefined and null runtypes (props to @pabra)

4.0.1

  • fix broken runtypes for string and integer due to missing return type annotations

4.0.0

  • use copy-on-write for performance: they mostly just check their input object and return it unmodified. Before that, arrays and objects were always copied. Now, only if the runtype or any nested runtype modifies its input (e.g. trimming strings or custom runtypes), objects and arrays are copied.
  • stringIndex and numberIndex now err on "__proto__" and Symbol keys

3.0.0

  • add explicit options (min, max, length etc.) to string, number, integer, stringAsInteger and array runtypes
  • change error messages
  • improve stringAsInteger to accept '-0' and '+123' (leading plus) as valid integer strings

2.0.0

  • remove .check and make user invocations of runtypes throw exceptions (using .check turned out to be a major annoyance and I forgot it in almost half of my code bc typescript does not warn me)
  • add runtype to create custom runtypes
  • add RuntypeUsageError, thrown when the api is misused
  • add getFormattedError, getFormattedErrorPath and getFormattedErrorValue functions to extract information from RuntypeError

1.0.0

  • add .check method throw a RuntypeException
  • add fail, isFail to replace throwing RuntypeException on runtype errors
  • add union, make intersection universal for all runtypes
  • add pick and omit combinators