|
1 |
| -# @jcoreio/typescript-validators |
| 1 | +# typescript-validators |
2 | 2 |
|
3 | 3 | [](https://circleci.com/gh/jcoreio/typescript-validators)
|
4 | 4 | [](https://codecov.io/gh/jcoreio/typescript-validators)
|
5 | 5 | [](https://github.com/semantic-release/semantic-release)
|
6 | 6 | [](http://commitizen.github.io/cz-cli/)
|
7 |
| -[](https://badge.fury.io/js/%40jcoreio%2Ftypescript-validators) |
| 7 | +[](https://badge.fury.io/js/typescript-validators) |
8 | 8 |
|
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. |
10 | 10 | The validation errors are detailed. Adapted from the brilliant work in `flow-runtime`.
|
11 | 11 |
|
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). |
0 commit comments