Skip to content

Commit 5dd9ee0

Browse files
committed
fix: more API tweaks and docs
1 parent 048e89c commit 5dd9ee0

12 files changed

+687
-409
lines changed

README.md

+310-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,318 @@
1-
# @jcoreio/typescript-validators
1+
# typescript-validators
22

33
[![CircleCI](https://circleci.com/gh/jcoreio/typescript-validators.svg?style=svg)](https://circleci.com/gh/jcoreio/typescript-validators)
44
[![Coverage Status](https://codecov.io/gh/jcoreio/typescript-validators/branch/master/graph/badge.svg)](https://codecov.io/gh/jcoreio/typescript-validators)
55
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
7-
[![npm version](https://badge.fury.io/js/%40jcoreio%2Ftypescript-validators.svg)](https://badge.fury.io/js/%40jcoreio%2Ftypescript-validators)
7+
[![npm version](https://badge.fury.io/js/typescript-validators.svg)](https://badge.fury.io/js/typescript-validators)
88

9-
Helps you write validators for TypeScript types, ensuring compile errors if your validator doesn't match the type.
9+
Complex type validators that generate TypeScript types for you.
1010
The validation errors are detailed. Adapted from the brilliant work in `flow-runtime`.
1111

12-
WIP
12+
# Table of Contents
13+
14+
<!-- toc -->
15+
16+
- [Introduction](#introduction)
17+
- [What about generating validators from type defs?](#what-about-generating-validators-from-type-defs)
18+
- [API](#api)
19+
- [Type creators](#type-creators)
20+
- [`t.boolean()`](#tboolean)
21+
- [`t.boolean(true)`](#tbooleantrue)
22+
- [`t.string()`](#tstring)
23+
- [`t.string('foo')`](#tstringfoo)
24+
- [`t.number()`](#tnumber)
25+
- [`t.number(3)`](#tnumber3)
26+
- [`t.symbol()`](#tsymbol)
27+
- [`t.symbol(MySymbol)`](#tsymbolmysymbol)
28+
- [`t.null()` / `t.nullLiteral()`](#tnull--tnullliteral)
29+
- [`t.undefined()` / `t.undefinedLiteral()`](#tundefined--tundefinedliteral)
30+
- [`t.nullish()`](#tnullish)
31+
- [`t.nullOr(t.string())`](#tnullortstring)
32+
- [`t.array(t.number())`](#tarraytnumber)
33+
- [`t.simpleObject({ foo: t.string() })`](#tsimpleobject-foo-tstring-)
34+
- [`t.object`](#tobject)
35+
- [`t.record(t.string(), t.number())`](#trecordtstring-tnumber)
36+
- [`t.tuple(t.string(), t.number())`](#ttupletstring-tnumber)
37+
- [`t.intersection(A, B)`](#tintersectiona-b)
38+
- [`t.union(t.string(), t.number())`](#tuniontstring-tnumber)
39+
- [`t.constrain(type, ...constraints)`](#tconstraintype-constraints)
40+
- [`t.Type`](#ttype)
41+
- [`accepts(input: any): boolean`](#acceptsinput-any-boolean)
42+
- [`assert(input: any, prefix = '', path?: (string | number | symbol)[]): V`](#assertinput-any-prefix---path-string--number--symbol-v)
43+
- [`validate(input: any, prefix = '', path?: (string | number | symbol)[]): Validation`](#validateinput-any-prefix---path-string--number--symbol-validation)
44+
- [`warn(input: any, prefix = '', path?: (string | number | symbol)[]): void`](#warninput-any-prefix---path-string--number--symbol-void)
45+
- [`toString(): string`](#tostring-string)
46+
47+
<!-- tocstop -->
48+
49+
# Introduction
50+
51+
When you need to validate the inputs to a TypeScript API, a problem arises. How do you ensure that a value that passes validation
52+
matches your declared TypeScript type? Someone might modify one and forget to modify the other:
53+
54+
```ts
55+
type Post = {
56+
author: {
57+
name: string
58+
username: string
59+
}
60+
content: string
61+
// newly added by developer
62+
tags: string[]
63+
}
64+
65+
// hypothetical syntax
66+
const validator = requireObject({
67+
author: requireObject({
68+
name: requireString(),
69+
username: requireString(),
70+
}),
71+
content: requireString(),
72+
// uhoh!! developer forgot to add tags here
73+
})
74+
```
75+
76+
`typescript-validators` solves this by generating TypeScript types from your validators:
77+
78+
```ts
79+
import * as t from 'typescript-validators'
80+
81+
const PostValidator = t.simpleObject({
82+
author: t.simpleObject({
83+
name: t.string(),
84+
username: t.string(),
85+
}),
86+
content: t.string(),
87+
tags: t.array(t.string()),
88+
})
89+
90+
type Post = t.ExtractType<typeof PostValidator>
91+
92+
const example: Post = PostValidator.assert({
93+
author: {
94+
name: 'MC Hammer',
95+
username: 'hammertime',
96+
},
97+
content: "Can't touch this",
98+
tags: ['mc-hammer', 'hammertime'],
99+
})
100+
```
101+
102+
Hover over `Post` in the IDE and you'll see, voilà:
103+
104+
```ts
105+
type Post = {
106+
author: {
107+
name: string
108+
username: string
109+
}
110+
content: string
111+
tags: string[]
112+
}
113+
```
114+
115+
# What about generating validators from type defs?
116+
117+
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
118+
`babel-pluging-flow-runtime`, which I have a lot of experience with. That looks like this:
119+
120+
```js
121+
import {type Type, reify} from 'flow-runtime'
122+
123+
type Post = {
124+
author: {
125+
name: string
126+
username: string
127+
}
128+
content: string
129+
tags: string[]
130+
}
131+
132+
const PostValidator = (reify: Type<Post>) // looooots of magic here
133+
134+
const example: Post = PostValidator.assert({
135+
author: {
136+
name: 'MC Hammer',
137+
username: 'hammertime',
138+
},
139+
content: "Can't touch this",
140+
tags: ['mc-hammer', 'hammertime'],
141+
})
142+
```
143+
144+
This is sweet but there are some caveats:
145+
146+
- You have to add a Babel plugin to your toolchain (for TypeScript, not everyone wants to use Babel)
147+
- There are issues with the Babel plugin. It aims to support all Flow types, with varying success.
148+
- The original author of `flow-runtime` abandoned the project and I don't blame him. It was hugely ambitious and difficult to maintain.
149+
150+
The author of `flow-runtime` himself told me in private conversations that he had moved on to an approach like
151+
`typescript-validators` in his own projects, because generating types from the validator declarations is a lot
152+
simpler and more maintainable in the long run.
153+
154+
# API
155+
156+
I recommend importing like this:
157+
158+
```ts
159+
import * as t from 'typescript-validators'
160+
```
161+
162+
## Type creators
163+
164+
All of the following methods return an instance of `t.Type<T>`.
165+
166+
### `t.boolean()`
167+
168+
A validator that requires the value to be a `boolean`.
169+
170+
### `t.boolean(true)`
171+
172+
A validator that requires the value to be `true`.
173+
174+
### `t.string()`
175+
176+
A validator that requires the value to be a `string`.
177+
178+
### `t.string('foo')`
179+
180+
A validator that requires the value to be `'foo'`.
181+
182+
### `t.number()`
183+
184+
A validator that requires the value to be a `number`.
185+
186+
### `t.number(3)`
187+
188+
A validator that requires the value to be `3`.
189+
190+
### `t.symbol()`
191+
192+
A validator that requires the value to be a `symbol`.
193+
194+
### `t.symbol(MySymbol)`
195+
196+
A validator that requires the value to be `MySymbol`.
197+
198+
### `t.null()` / `t.nullLiteral()`
199+
200+
A validator that requires the value to be `null`.
201+
202+
### `t.undefined()` / `t.undefinedLiteral()`
203+
204+
A validator that requires the value to be `undefined`.
205+
206+
### `t.nullish()`
207+
208+
A validator that requires the value to be `null | undefined`.
209+
210+
### `t.nullOr(t.string())`
211+
212+
A validator that requires the value to be `string | null`
213+
214+
### `t.array(t.number())`
215+
216+
A validator that requires the value to be `number[]`.
217+
218+
### `t.simpleObject({ foo: t.string() })`
219+
220+
A validator that requires the value to be an object with only a `foo` property that's a `string`.
221+
222+
### `t.object`
223+
224+
For dealing with optional properties, use the following.
225+
The syntax is a bit awkward but it's the best way I could find to get a clean type output:
226+
227+
```ts
228+
const ThingValidator = t.object<{
229+
name: any
230+
comment?: any
231+
}>()({
232+
name: t.string(),
233+
comment: t.optional(t.string()),
234+
})
235+
236+
type Thing = t.ExtractType<typeof ThingValidator>
237+
```
238+
239+
The type of `Thing` will be `{ name: string, comment?: string }`. Note that the property types in the explicit type parameter
240+
(`any`) are ignored. The type parameter just indicates which
241+
properties are required and which are optional.
242+
243+
You can also use the `t.optionalNullOr(t.string())` as a shorthand for
244+
`t.optional(t.nullOr(t.string()))`.
245+
246+
### `t.record(t.string(), t.number())`
247+
248+
A validator that requires the value to be `Record<string, number>`.
249+
250+
### `t.tuple(t.string(), t.number())`
251+
252+
A validator that requires the value to be `[string, number]`.
253+
Accepts a variable number of arguments.
254+
255+
### `t.intersection(A, B)`
256+
257+
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:
258+
259+
```ts
260+
const ThingType = t.simpleObject({ name: t.string() })
261+
const CommentedType = t.simpleObject({ comment: t.string() })
262+
263+
const CommentedThingType = t.intersection(ThingType, CommentedType)
264+
265+
CommentedThingType.assert({ name: 'foo', comment: 'sweet' })
266+
```
267+
268+
### `t.union(t.string(), t.number())`
269+
270+
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.
271+
272+
### `t.constrain(type, ...constraints)`
273+
274+
Applies custom constraints to a type. For example:
275+
276+
```ts
277+
const PositiveNumber = t.constrain(t.number(), (value: number):
278+
| string
279+
| null
280+
| undefined => {
281+
if (value < 0) return 'must be >= 0'
282+
})
283+
284+
PositiveNumber.assert(-1) // throws an error including "must be >= 0"
285+
```
286+
287+
## `t.Type<T>`
288+
289+
The base class for all validator types.
290+
291+
`T` is the type of values it accepts.
292+
293+
### `accepts(input: any): boolean`
294+
295+
Returns `true` if and only if `input` is the correct type.
296+
297+
### `assert<V extends T>(input: any, prefix = '', path?: (string | number | symbol)[]): V`
298+
299+
Throws an error if `input` isn't the correct type.
300+
301+
`prefix` will be prepended to thrown error messages.
302+
303+
`path` will be prepended to validation error paths. If you are validating a function parameter named `foo`,
304+
pass `['foo']` for `path` to get clear error messages.
305+
306+
### `validate(input: any, prefix = '', path?: (string | number | symbol)[]): Validation<T>`
307+
308+
Validates `input`, returning any errors in the `Validation`.
309+
310+
`prefix` and `path` are the same as in `assert`.
311+
312+
### `warn(input: any, prefix = '', path?: (string | number | symbol)[]): void`
313+
314+
Logs a warning to the console if `input` isn't the correct type.
315+
316+
### `toString(): string`
317+
318+
Returns a string representation of this type (using TS type syntax in most cases).

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "@jcoreio/typescript-validators",
2+
"name": "typescript-validators",
33
"version": "0.0.0-development",
4-
"description": "API input validators with user-friendly error output and TypeScript to ensure you don't miss any properties",
4+
"description": "complex type validators that generate TypeScript types for you",
55
"main": "index.js",
66
"sideEffects": false,
77
"scripts": {

src/Validation.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type ErrorTuple = [IdentifierPath, string, Type<any>]
99
export default class Validation<T> {
1010
input: T
1111

12-
path: string[] = []
12+
path: IdentifierPath = []
1313

1414
prefix = ''
1515

src/compareTypes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ export default function compareTypes(a: Type<any>, b: Type<any>): -1 | 0 | 1 {
3030
return result
3131
}
3232
}
33+
34+
// this is done to avoid circular import problems
35+
Type.__compareTypes = compareTypes

src/errorReporting/typeOf.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
function keyToString(key: string | number | symbol): string {
2+
switch (typeof key) {
3+
case 'symbol':
4+
return `[${String(key)}]`
5+
case 'number':
6+
return String(key)
7+
case 'string':
8+
if (/^[_a-z][_a-z0-9]*$/i.test(key)) return key
9+
}
10+
return JSON.stringify(key)
11+
}
12+
113
export default function typeOf(value: any): string {
214
if (value == null) return String(value)
315
if (typeof value !== 'object') return typeof value
416
if (value.constructor && value.constructor !== Object)
517
return value.constructor.name
618
return `{\n${Object.keys(value)
7-
.map(key => ` ${key}: ${typeOf(value[key])}`)
19+
.map(key => ` ${keyToString(key)}: ${typeOf(value[key])}`)
820
.join(',\n')}\n}`
921
}

0 commit comments

Comments
 (0)