typescript-validators
Complex type validators that generate TypeScript types for you. The validation errors are detailed. Adapted from the brilliant work in flow-runtime
.
Table of Contents
- Introduction
- What about generating validators from type defs?
- API
- Type creators
t.boolean()
t.boolean(true)
t.string()
t.string('foo')
t.number()
t.number(3)
t.symbol()
t.symbol(MySymbol)
t.null()
/t.nullLiteral()
t.nullOr(t.string())
t.undefined()
/t.undefinedLiteral()
t.nullish()
t.nullishOr(t.string())
t.array(t.number())
t.simpleObject({ foo: t.string() })
t.object
t.record(t.string(), t.number())
t.instanceOf(Date)
t.tuple(t.string(), t.number())
t.allOf(A, B)
t.oneOf(t.string(), t.number())
t.alias(name, type)
t.ref(() => typeAlias)
t.Type<T>
t.ExtractType<T extends Type<any>>
t.TypeAlias<T>
- Custom Constraints
- Recursive Types
- Type creators
Introduction
When you need to validate the inputs to a TypeScript API, a problem arises. How do you ensure that a value that passes validation matches your declared TypeScript type? Someone might modify one and forget to modify the other:
typescript-validators
solves this by generating TypeScript types from your validators:
Hover over Post
in VSCode and you'll see, voilà:
What about generating validators from type defs?
I'd like to be able to do this, because type defs are a lot more readable. In fact, for Flow, it's possible with babel-pluging-flow-runtime
, which I have a lot of experience with. That looks like this:
type Post = author: name: string username: string content: string tags: string const PostValidator = reify: Type<Post> // looooots of magic here const example: Post = PostValidator
This is sweet but there are some caveats:
- You have to add a Babel plugin to your toolchain (for TypeScript, not everyone wants to use Babel)
- There are issues with the Babel plugin. It aims to support all Flow types, with varying success.
- The original author of
flow-runtime
abandoned the project and I don't blame him. It was hugely ambitious and difficult to maintain.
The author of flow-runtime
himself told me in private conversations that he had moved on to an approach like typescript-validators
in his own projects, because generating types from the validator declarations is a lot simpler and more maintainable in the long run.
API
I recommend importing like this:
Type creators
All of the following methods return an instance of t.Type<T>
.
t.boolean()
A validator that requires the value to be a boolean
.
t.boolean(true)
A validator that requires the value to be true
.
t.string()
A validator that requires the value to be a string
.
t.string('foo')
A validator that requires the value to be 'foo'
.
t.number()
A validator that requires the value to be a number
.
t.number(3)
A validator that requires the value to be 3
.
t.symbol()
A validator that requires the value to be a symbol
.
t.symbol(MySymbol)
A validator that requires the value to be MySymbol
.
t.null()
/ t.nullLiteral()
A validator that requires the value to be null
.
t.nullOr(t.string())
A validator that requires the value to be string | null
t.undefined()
/ t.undefinedLiteral()
A validator that requires the value to be undefined
.
t.nullish()
A validator that requires the value to be null | undefined
.
t.nullishOr(t.string())
A validator that requires the value to be string | null | undefined
.
t.array(t.number())
A validator that requires the value to be number[]
.
t.simpleObject({ foo: t.string() })
A validator that requires the value to be an object with only a foo
property that's a string
.
t.object
For dealing with optional properties, use the following. The syntax is a bit awkward but it's the best way I could find to get a clean type output:
The type of Thing
will be { name: string, comment?: string }
. Note that the property types in the explicit type parameter (any
) are ignored. The type parameter just indicates which properties are required and which are optional, and also allows you to mark properties readonly. These attributes will be reflected in t.ExtractType
.
You can also use the t.optionalNullOr(t.string())
as a shorthand for t.optional(t.nullOr(t.string()))
.
t.record(t.string(), t.number())
A validator that requires the value to be Record<string, number>
.
t.instanceOf(Date)
A validator that requires the value to be an instance of Date
.
t.tuple(t.string(), t.number())
A validator that requires the value to be [string, number]
. Accepts a variable number of arguments.
t.allOf(A, B)
A validator that requires the value to be A & B
. Accepts a variable number of arguments, though type generation is only overloaded up to 8 arguments. For example:
CommentedThingType.assert
t.oneOf(t.string(), t.number())
A validator that requires the value to be string | number
. Accepts a variable number of arguments, though type generation is only overloaded up to 8 arguments.
t.alias(name, type)
Creates a TypeAlias
with the given name
and type
.
Type aliases serve two purposes:
- They allow you to create recursive type validators with
t.ref()
- You can add custom constraints to them
t.ref(() => typeAlias)
Creates a reference to the given TypeAlias
. See Recursive Types for examples.
t.Type<T>
The base class for all validator types.
T
is the type of values it accepts.
accepts(input: any): boolean
Returns true
if and only if input
is the correct type.
assert<V extends T>(input: any, prefix = '', path?: (string | number | symbol)[]): V
Throws an error if input
isn't the correct type.
prefix
will be prepended to thrown error messages.
path
will be prepended to validation error paths. If you are validating a function parameter named foo
, pass ['foo']
for path
to get clear error messages.
validate(input: any, prefix = '', path?: (string | number | symbol)[]): Validation<T>
Validates input
, returning any errors in the Validation
.
prefix
and path
are the same as in assert
.
warn(input: any, prefix = '', path?: (string | number | symbol)[]): void
Logs a warning to the console if input
isn't the correct type.
toString(): string
Returns a string representation of this type (using TS type syntax in most cases).
t.ExtractType<T extends Type<any>>
Gets the TypeScript type that a validator type accepts. For example:
Hover over Post
in the IDE and you'll see, voilà:
t.TypeAlias<T>
readonly name: string
The name of the alias.
addConstraint(...constraints: TypeConstraint<T>[]): this
Adds custom constraints. TypeConstraint<T>
is a function (value: T) => string | null | undefined
which returns nullish if value
is valid, or otherwise a string
describing why value
is invalid.
Custom Constraints
It's nice to be able to validate that something is a number
, but what if we want to make sure it's positive? We can do this by creating a type alias for number
and adding a custom constraint to it:
.alias'PositiveNumber', t.number .addConstraintvalue > 0 ? undefined : 'must be > 0' PositiveNumberType.assert-1
The assertion will throw a t.RuntimeTypeError
with the following message:
Value must be > 0
Expected: PositiveNumber
Actual Value: -1
Actual Type: number
Recursive Types
Creating validators for recursive types takes a bit of extra effort. Naively, we would want to do this:
But t.optional(NodeType)
causes the error Block-scoped variable 'NodeType' referenced before its declaration
.
To work around this, we can create a TypeAlias
and a reference to it:
Notice how we use a thunk function in t.ref(() => NodeType)
to avoid referencing NodeType
before its declaration.