Preface
I said I want SIMPLE runtypes.
Just functions that validate and return data.
Combine them into complex types and Typescript knows their structure.
Thats how runtypes work.
Install
npm install simple-runtypes
or yarn add simple-runtypes
Example
- 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
}
- Use the runtype to validate untrusted data
userRuntype({id: 1, name: 'matt'})
userRuntype({id: 1, name: 'matt', isAdmin: true})
You can also use
a runtype without throwing errors:
st.use(userRuntype, {id: 1, name: 'matt'})
st.use(userRuntype, {id: 1, name: 'matt', isAdmin: true})
st.getFormattedError(FAIL)
Not throwing errors is way more efficient but less convenient as you always
have to check the resulting type.
Why?
Why should I use this over the plethora of other runtype validation libraries available?
- Written in and for Typescript
- Strict by default
- Supports efficient discriminated unions
- Frontend-friendly (no
eval
, small footprint, no dependencies) - Fast (of all non-eval based libs, only one is faster according to the benchmark)
Benchmarks
@moltar has done a great job comparing existing runtime typechecking libraries in moltar/typescript-runtime-type-benchmarks
Documentation
Intro
A Runtype
is a function that:
- receives an unknown value
- returns that value or a copy if all validations pass
- 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 retuns 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})
To ignore single properties, use ignore
, unknown
or any
:
const strict = st.record({name: st.string(), other: st.ignore()})
strict({name: 'foo', other: 123})
Use sloppyRecord
to only validate known properties and remove everything else:
const sloppy = st.sloppyRecord({name: st.string()})
strict({name: 'foo', other: 123, bar: []})
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.recorcd({ id: st.integer, label: st.string() })),
})
nestedRuntype({
name: 'foo',
items: [{ id: 3, label: 'bar' }],
})
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<networkStateRuntype>
Finding the runtype to validate a specific discriminating union with is done efficiently with a Map
.
Reference
Basic runtypes that match TS / Javascript types:
Meta runtypes
Objects and Array Runtypes
Advanced Runtypes
Roadmap / Todos
- rename
sloppyRecord
to record.sloppy
because I need
the "sloppy"-concept for other runtypes too: e.g. nullable.sloppy
- a
Runtype<T | null>
that also accepts undefined
which is useful to slowly
add new nullable fields to existing json database records - improve docs:
- preface: what is a runtype and why is it useful
- why: explain or link to example that shows "strict by default" and "efficient discriminating unions"
- show that simple-runtypes is feature complete because it can
- express all typescript types
- is extendable with custom runtypes (add documentation)
- add small frontend and backend example projects that show how to use simple-runtypes in production
- test types with tsd
- add missing combinators: partial, required
- add other combinators like partial, required, ...