├── .babelrc ├── .eslintrc ├── .gitignore ├── .node-version ├── .npmignore ├── .prettierrc ├── .yarn-version ├── LICENSE ├── README.md ├── package.json ├── src ├── apiFunctions.js ├── client-test.js ├── coercion.js ├── formatValue.js ├── index.js ├── index.ts ├── installGlobals.js ├── makeError.js ├── typeConstructors │ ├── array.js │ ├── arrayContaining.js │ ├── exactNumber.js │ ├── exactString.js │ ├── func.js │ ├── index.js │ ├── instanceOf.js │ ├── intersection.js │ ├── map.js │ ├── maybe.js │ ├── object.js │ ├── objectMap.js │ ├── predicate.js │ ├── set.js │ ├── shape.js │ ├── stringMatching.js │ ├── symbolFor.js │ ├── tuple.js │ ├── tuple.test.js │ └── union.js └── types │ ├── Buffer.js │ ├── Buffer.test.js │ ├── Date.js │ ├── Element.js │ ├── Error.js │ ├── Function.js │ ├── Infinity.js │ ├── NaN.js │ ├── NegativeInfinity.js │ ├── RegExp.js │ ├── Symbol.js │ ├── URL.js │ ├── any.js │ ├── anyObject.js │ ├── boolean.js │ ├── boolean.test.js │ ├── false.js │ ├── index.js │ ├── integer.js │ ├── makeTypedArrayDef.js │ ├── never.js │ ├── null.js │ ├── number.js │ ├── string.js │ ├── true.js │ └── undefined.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["unobtrusive", "unobtrusive/flowtype"], 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "browser": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .DS_Store 3 | node_modules 4 | yarn-error.log 5 | .docz 6 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v12.22.1 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .babelrc 3 | .eslintrc 4 | .gitignore 5 | .prettierrc 6 | yarn-error.log 7 | .DS_Store 8 | docs 9 | docs_src 10 | doczrc.js 11 | webpack.config.js 12 | .flowconfig 13 | .docz 14 | *.test.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.yarn-version: -------------------------------------------------------------------------------- 1 | 1.22.17 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Suchipi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serializable-types 2 | 3 | `serializable-types` is a combination runtime type checker and value (de)serializer for node and the browser. 4 | 5 | It's kind of like [`prop-types`](https://npm.im/prop-types), [`flow-runtime`](https://npm.im/flow-runtime), or [`ow`](https://npm.im/ow), but it can use its type awareness to serialize and deserialize values into JSON-safe objects, kinda like [`ejson`](https://npm.im/ejson). 6 | 7 | ## Code Example 8 | 9 | ```js 10 | import * as t from "serializable-types"; 11 | 12 | // -------------- 13 | // Checking types 14 | // -------------- 15 | 16 | t.isOfType(42, t.number); // true 17 | t.isOfType(42, t.boolean); // false 18 | t.isOfType(true, t.boolean); // true 19 | t.isOfType(true, t.number); // false 20 | 21 | t.isOfType([1, 2, 3], t.array(t.number)); // true 22 | 23 | t.isOfType( 24 | { 25 | foo: 42, 26 | bar: ["hi", 65], 27 | baz: [new Date(), Buffer.from("bla bla"), new Date()], 28 | }, 29 | t.object({ 30 | foo: t.number, 31 | bar: t.tuple(t.string, t.number), 32 | baz: t.array(t.union(t.Date, t.Buffer)), 33 | }) 34 | ); // true 35 | 36 | // --------------- 37 | // Asserting types 38 | // --------------- 39 | 40 | t.assertType(true, t.boolean); // No error 41 | t.assertType(42, t.boolean); // throws TypeError: Expected boolean, but received 42​​ 42 | 43 | t.assertType([true, false, null], t.array(t.boolean)); 44 | // throws TypeError: ​​Expected Array, but received: [ true, false, null ] 45 | 46 | t.assertType( 47 | { 48 | bad: true, 49 | }, 50 | t.object({ 51 | foo: t.number, 52 | bar: t.tuple(t.string, t.number), 53 | baz: t.array(t.union(t.Date, t.Buffer)), 54 | }) 55 | ); 56 | // TypeError: Expected { foo: number, bar: [string, number], baz: Array }, but received: { "bad" : true } 57 | 58 | // ---------------------------------- 59 | // Serializing values of a known type 60 | // ---------------------------------- 61 | 62 | t.serializeWithType(Buffer.from("hello"), t.Buffer); 63 | // { $type: "Buffer", $value: [104, 101, 108, 108, 111] } 64 | 65 | t.serializeWithType( 66 | { 67 | data: Buffer.from([104, 101, 108, 108, 111]), 68 | encoding: "utf-8", 69 | }, 70 | t.object({ 71 | data: t.union(t.Buffer, t.string), 72 | encoding: t.string, 73 | }) 74 | ); 75 | // { 76 | // $type: "object", 77 | // $value: { 78 | // data: { 79 | // $type: "Buffer", 80 | // $value: [104, 101, 108, 108, 111], 81 | // }, 82 | // encoding: { $type: "string", $value: "utf-8" }, 83 | // }, 84 | // } 85 | 86 | // ------------------------------------ 87 | // Deserializing values of a known type 88 | // ------------------------------------ 89 | 90 | t.deserializeWithType( 91 | { 92 | $type: "object", 93 | $value: { 94 | data: { 95 | $type: "Buffer", 96 | $value: [104, 101, 108, 108, 111], 97 | }, 98 | encoding: { $type: "string", $value: "utf-8" }, 99 | }, 100 | }, 101 | t.object({ 102 | data: t.union(t.Buffer, t.string), 103 | encoding: t.string, 104 | }) 105 | ); 106 | // { data: , encoding: "utf-8" } 107 | ``` 108 | 109 | Try it out on [RunKit](https://npm.runkit.com/serializable-types)! 110 | 111 | ## API Overview 112 | 113 | The `types` namespace exported by this module has three kinds of objects on it: `TypeDef`s, functions which create `TypeDef`s (hereafter known as "`TypeDef` constructors"), and functions you use to work with `TypeDef`s (hereafter known as "utility functions"). 114 | 115 | A `TypeDef` is an object that represents a given type within JavaScript, that has methods on it that can be used to work with values of that type. 116 | 117 | Every `TypeDef` has this shape: 118 | 119 | ```ts 120 | interface TypeDef { 121 | // Human-readable description of the type, eg `Buffer` or `Array` 122 | description: string; 123 | 124 | // Human-readbale description of the type this serializes to, eg `{ $type: "Buffer", $value: Array }` 125 | serializedDescription: string; 126 | 127 | // Check if a given value is of this type. Returns true if it's this type, false otherwise. 128 | check(val: any): boolean; 129 | 130 | // Serialize the given value so that it can be encoded as JSON. 131 | // If the given value is not of this type, an error will be thrown. 132 | serialize(val: any): any; 133 | 134 | // Check if a given object can be deserialized to this type. True if it can, false otherwise. 135 | checkSerialized(serialized: any): boolean; 136 | 137 | // Deserialize the given object into this type. 138 | // If the given object cannot be deserialized into this type, an error will be thrown. 139 | deserialize(serialized: any): any; 140 | } 141 | ``` 142 | 143 | Here is a list of all the `TypeDef`s: 144 | 145 | - `t.any` 146 | - `t.anyObject` 147 | - `t.boolean` 148 | - `t.Buffer` 149 | - `t.Date` 150 | - `t.Element` 151 | - `t.Error` 152 | - `t.false` 153 | - `t.Function` 154 | - `t.Infinity` 155 | - `t.integer` 156 | - `t.NaN` 157 | - `t.NegativeInfinity` 158 | - `t.never` 159 | - `t.null` 160 | - `t.number` 161 | - `t.RegExp` 162 | - `t.string` 163 | - `t.Symbol` 164 | - `t.true` 165 | - `t.Int8Array` 166 | - `t.Uint8Array` 167 | - `t.Uint8ClampedArray` 168 | - `t.Int16Array` 169 | - `t.Uint16Array` 170 | - `t.Int32Array` 171 | - `t.Uint32Array` 172 | - `t.Float32Array` 173 | - `t.Float64Array` 174 | - `t.undefined` 175 | - `t.URL` 176 | 177 | Here is a list of all the `TypeDef` constructors: 178 | 179 | - `t.array(memberTypeDef)` 180 | - `t.arrayContaining(memberTypeDef)` 181 | - `t.exactNumber(num)` 182 | - `t.exactString(str)` 183 | - `t.func(params, returnValue)` 184 | - `t.instanceOf(klass)` 185 | - `t.intersection(...memberTypeDefs)` 186 | - `t.map(keyTypeDef, valueTypeDef)` 187 | - `t.maybe(typeDef)` 188 | - `t.object(typeDefObjectMap)` 189 | - `t.objectMap(valueTypeDef, keyTypeDef)` 190 | - `t.predicate(matcherFunction)` 191 | - `t.set(memberTypeDef)` 192 | - `t.shape(typeDefObjectMap)` 193 | - `t.stringMatching(regex)` 194 | - `t.symbolFor(tag)` 195 | - `t.tuple(...memberTypeDefs)` 196 | - `t.union(...memberTypeDefs)` 197 | 198 | And here is a list of all the utility functions: 199 | 200 | - `t.assertType(value, typeDef)` 201 | - `t.isOfType(value, typeDef)` 202 | - `t.serializeWithType(value, typeDef)` 203 | - `t.deserializeWithType(serialized, typeDef)` 204 | - `t.installGlobals()` 205 | - `t.coerceToType(value)` 206 | 207 | Each export is documented in further detail below. 208 | 209 | ## API: Utility Functions 210 | 211 | Note that these functions will work with any object that implements the `TypeDef` interface described above; so they can be used not only with `TypeDef`s from this package, but also custom `TypeDef`s you write. 212 | 213 | Additionally, these functions support "Automatic `TypeDef` Coercion", which means you can pass in placeholder values instead of `TypeDef` objects in many places. See the Notes section at the bottom of the README for more info. 214 | 215 | ### `t.assertType(value, typeDef)` 216 | 217 | A function that, given a value and a `TypeDef`, throws an error if the value is not of the type described by the `TypeDef`. 218 | 219 | ### `t.isOfType(value, typeDef)` 220 | 221 | A function that, given a value and a `TypeDef`, returns true if the value is of the type described by the `TypeDef`, and false otherwise. 222 | 223 | ### `t.serializeWithType(value, typeDef)` 224 | 225 | A function that, given a value and a `TypeDef`, serializes the value using the `TypeDef`, and returns the serialized value. If the value you are trying to serialize is not of the type described by the `TypeDef`, an error will be thrown. 226 | 227 | ### `t.deserializeWithType(serialized, typeDef)` 228 | 229 | A function that, given a serialized value and a `TypeDef`, deserializes the value using the `TypeDef`, and returns the deserialized value. If the value you are trying to deserialize is not of the serialized type described by the `TypeDef`, an error will be thrown. 230 | 231 | ### `installGlobals()` 232 | 233 | Makes several exports from this library global: 234 | 235 | - All the Utility Functions 236 | - All the type constructors 237 | - These specific `TypeDef` objects: 238 | - `boolean` 239 | - `integer` 240 | - `number` 241 | - `string` 242 | - `never` 243 | 244 | Not all `TypeDefs` are exported 245 | 246 | ## API: `TypeDef`s 247 | 248 | ### `t.any` 249 | 250 | A `TypeDef` which represents anything. Cannot be used for serialization/deserialization, but can be used for runtime type-checking. 251 | 252 | ### `t.anyObject` 253 | 254 | A `TypeDef` which represents any object. More specifically, any _non-falsy_ (not null) value whose type string obtained from using the `typeof` operator returns `"object"`. 255 | 256 | ### `t.boolean` 257 | 258 | A `TypeDef` which represents boolean values, either `true` or `false`. 259 | 260 | ### `t.Buffer` 261 | 262 | A `TypeDef` which represents a [`Buffer`](https://nodejs.org/api/buffer.html#buffer_buffer). 263 | 264 | ### `t.Date` 265 | 266 | A `TypeDef` which represents a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). 267 | 268 | ### `t.Element` 269 | 270 | A `TypeDef` which represents an [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element). 271 | 272 | ### `t.Error` 273 | 274 | A `TypeDef` which represents an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). 275 | 276 | ### `t.false` 277 | 278 | A `TypeDef` which represents the value `false`. 279 | 280 | ### `t.Function` 281 | 282 | A `TypeDef` which represents a `Function`. Note that functions cannot be (de)serialized. 283 | 284 | ### `t.Infinity` 285 | 286 | A `TypeDef` which represents the value `Infinity`. 287 | 288 | ### `t.integer` 289 | 290 | A `TypeDef` which represents integers. 291 | 292 | ### `t.NaN` 293 | 294 | A `TypeDef` which represents the value `NaN`. 295 | 296 | ### `t.NegativeInfinity` 297 | 298 | A `TypeDef` which represents the value `-Infinity`. 299 | 300 | ### `t.never` 301 | 302 | A `TypeDef` that no value satisfies. 303 | 304 | ### `t.null` 305 | 306 | A `TypeDef` which represents the value `null`. 307 | 308 | ### `t.number` 309 | 310 | A `TypeDef` which represents all numbers except `NaN`, `Infinity`, and `-Infinity`. 311 | 312 | ### `t.RegExp` 313 | 314 | A `TypeDef` which represents a [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). 315 | 316 | ### `t.string` 317 | 318 | A `TypeDef` which represents any string value. 319 | 320 | ### `t.Symbol` 321 | 322 | A `TypeDef` which represents a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). Note: only shared Symbols from the global Symbol registry can be (de)serialized. 323 | 324 | ### `t.true` 325 | 326 | A `TypeDef` which represents the value `true`. 327 | 328 | ### `t.Int8Array`, `t.Uint8Array`, `t.Uint8ClampedArray`, `t.Int16Array`, `t.Uint16Array`, `t.Int32Array`, `t.Uint32Array`, `t.Float32Array`, `t.Float64Array` 329 | 330 | `TypeDef`s which represent [typed array views](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays). 331 | 332 | ### `t.undefined` 333 | 334 | A `TypeDef` which represents the value `undefined`. 335 | 336 | ### `t.URL` 337 | 338 | A `TypeDef` which represents a WHATWG [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object. 339 | 340 | ## API: `TypeDef` constructors 341 | 342 | ### `t.array(memberTypeDef)` 343 | 344 | A function which returns a `TypeDef` which represents a homogenous Array of the given type; for example, `t.array(t.boolean)` represents an Array of unbounded length containing only booleans. This is like `Array` in Flow/TypeScript. 345 | 346 | ### `t.arrayContaining(memberTypeDef)` 347 | 348 | A function which returns a `TypeDef` which represents an Array containing at least one of the given type; for example, `t.arrayContaining(t.string)` represents an Array of undetermined length containing at least one string. This is like `Array` in Flow/TypeScript. 349 | 350 | ### `t.exactNumber(num)` 351 | 352 | A function which returns a `TypeDef` which represents an exact number. For example, `t.exactNumber(42)`. This is most useful when combined with `t.union` to simulate enums; for example, `t.union(t.exactNumber(0), t.exactNumber(1))`, which is like `0 | 1` in Flow/TypeScript. 353 | 354 | ### `t.exactString(str)` 355 | 356 | A function which returns a `TypeDef` which represents an exact string. For example, `t.exactString("foo")`. This is most useful when combined with `t.union` to simulate enums; for example, `t.union(t.exactString("GET"), t.exactString("POST"))`, which is like `"GET" | "POST"` in Flow/TypeScript. 357 | 358 | ### `t.func(params, returnType)` 359 | 360 | A function which returns a `TypeDef` which represents a function with the given parameter type(s) and return type. 361 | 362 | Note that when using thie `TypeDef`, the parameter types and return type are _NOT_ actually checked, because there is no way to do so in JavaScript at runtime without annotating or wrapping every function. As such, at runtime, this `TypeDef` only checks that the value is a Function. However, it may still useful to use instead of `Function` if you do not have a static type system in your codebase and want to document things for future readers. 363 | 364 | ### `t.instanceOf(klass)` 365 | 366 | A function which returns a `TypeDef` which represents an instance of the provided klass. 367 | 368 | Note that serialization will not be supported with this `TypeDef` unless the class implements the three following static methods: 369 | 370 | - `static serialize(instance)`, which should serialize the instance into a JSON-compatible format (like an Object) 371 | - `static checkSerialized(anyValue)`, which should return a boolean indicating whether some given value is of the type that `serialize` returns 372 | - `static deserialize(serializedInstance)`, which should return an instance of the class by using the serialized representation. 373 | 374 | ### `t.intersection(...memberTypeDefs)` 375 | 376 | A function which returns a `TypeDef` which represents the intersection of the given `TypeDef`s. It's kind of like a logical "AND". For example: 377 | 378 | ```js 379 | t.intersection( 380 | t.object({ 381 | foo: t.number, 382 | }), 383 | t.object({ 384 | bar: t.number, 385 | }) 386 | ); 387 | ``` 388 | 389 | This is like `{ foo: number } & { bar: number }` in Flow/TypeScript. 390 | 391 | ### `t.map(keyTypeDef, valueTypeDef)` 392 | 393 | A function which returns a `TypeDef` which represents a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), containing the given key and value types. For example, `t.map(t.string, t.Buffer)`. This is like `Map` in Flow/TypeScript. 394 | 395 | ### `t.maybe(typeDef)` 396 | 397 | A function which returns a `TypeDef` which represents the union between the given `TypeDef` and `undefined`. For example: `t.maybe(t.string)`. This is similar to `?string` in Flow and `string?` in TypeScript. 398 | 399 | This is most useful in object t.for representing optional properties, eg: 400 | 401 | ```js 402 | t.object({ 403 | size: t.number, 404 | data: t.maybe(t.Buffer), 405 | }); 406 | ``` 407 | 408 | ### `t.object(typeDefObjectMap)` 409 | 410 | A function which returns a `TypeDef` which represents an Object whose properties are typed by the passed `typeDefObjectMap`. For example: 411 | 412 | ```js 413 | t.object({ 414 | size: t.number, 415 | data: t.union(t.string, t.Buffer), 416 | encoding: t.maybe(t.string), 417 | }); 418 | 419 | // This is similar to the following in Flow/TypeScript: 420 | // { 421 | // size: number, 422 | // data: string | Buffer, 423 | // encoding?: string 424 | // } 425 | ``` 426 | 427 | ### `t.objectMap(valueTypeDef, keyTypeDef?)` 428 | 429 | A function which returns a `TypeDef` which represents an Object whose keys are arbitrary and whose values are the same. For example: 430 | 431 | ```js 432 | t.objectMap(t.number, t.string); 433 | // This is similar to `{ [string]: number }` in Flow/TypeScript. 434 | ``` 435 | 436 | Note that the value `TypeDef` is the first argument and the key `TypeDef` is the second argument, which may be somewhat unintuitive. This is because the key `TypeDef` is optional and defaults to `t.union(t.string, t.Symbol)`. 437 | 438 | ```js 439 | t.objectMap(t.number); 440 | // This is similar to `{ [string | Symbol]: number }` in Flow/TypeScript. 441 | ``` 442 | 443 | ### `t.predicate(matcherFunction)` 444 | 445 | A function that returns a `TypeDef` which represents any value for which the provided `matcherFunction` returns true. You can use this to create TypeDefs that conform to any user-defined bounds. However, be aware that a TypeDef returned from `t.predicate` cannot be used for (de)serialization; it can only be used for runtime type checking. 446 | 447 | ### `t.set(memberTypeDef)` 448 | 449 | A function which returns a `TypeDef` which represents a [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set), containing the given member type. For example, `t.set(t.string)`. This is like `Set` in Flow/TypeScript. 450 | 451 | ### `t.shape(typeDefObjectMap)` 452 | 453 | A function which is the same as `t.object` but all of the object properties in the returned `TypeDef` are wrapped with `t.maybe`. This is similar to `$Shape` in Flow, and is useful for config options, React Props, etc. 454 | 455 | ### `t.stringMatching(regex)` 456 | 457 | A function which returns a `TypeDef` which represents any string that matches the provided regular expression. 458 | 459 | ### `t.symbolFor(tag)` 460 | 461 | A function which returns a `TypeDef` which represents the value of calling `Symbol.for` with the provided tag string. 462 | 463 | ### `t.tuple(...memberTypeDefs)` 464 | 465 | A function which returns a `TypeDef` which represents an Array of fixed length with typed values at each index. For example: 466 | 467 | ```js 468 | t.tuple(t.string, t.number); 469 | // This is like `[string, number]` in Flow/TypeScript. 470 | ``` 471 | 472 | ### `t.union(...memberTypeDefs)` 473 | 474 | A function which returns a `TypeDef` which represents the union of the given `TypeDef`s. It kind of works like a logical "OR". For example: 475 | 476 | ```js 477 | t.union(t.string, t.number); 478 | // This is like `string | number` in Flow/TypeScript. 479 | ``` 480 | 481 | ## Notes 482 | 483 | ### Automatic `TypeDef` Coercion 484 | 485 | Every function in this library that accepts a `TypeDef` as one of its parameters support something called "Automatic `TypeDef` Coercion". That means that in place of a `TypeDef`, you can pass in a value from which a desired `TypeDef` can be inferred. This means that in many cases, you do not need to import a ton of `TypeDef`s from this library, but can instead use common globals or literals in their place. 486 | 487 | For instance, instead of writing this: 488 | 489 | ```js 490 | var someVariable = true; 491 | 492 | assertType(someVariable, t.boolean); 493 | ``` 494 | 495 | You could write this: 496 | 497 | ```js 498 | var someVariable = true; 499 | 500 | // `Boolean` is automatically "coerced" to `t.boolean` 501 | assertType(someVariable, Boolean); 502 | ``` 503 | 504 | Here is a list of values which will be automatically coerced to `TypeDef`s. The left side of the arrow indicates the value you pass in to the function, and the right side of the arrow indicates the `TypeDef` it will be coerced into. 505 | 506 | - `true` -> `t.true` 507 | - `false` -> `t.false` 508 | - `null` -> `t.null` 509 | - `undefined` -> `t.undefined` 510 | - `NaN` -> `t.NaN` 511 | - `Infinity` -> `t.Infinity` 512 | - `-Infinity` -> `t.NegativeInfinity` 513 | - `Object` -> `t.anyObject` 514 | - `t.object` (the function itself) -> `t.anyObject` 515 | - `URL` -> `t.URL` 516 | - `Symbol` -> `t.Symbol` 517 | - `RegExp` -> `t.RegExp` 518 | - `Function` -> `t.Function` 519 | - `Error` -> `t.Error` 520 | - `Element` -> `t.Element` 521 | - `Buffer` -> `t.Buffer` 522 | - `Date` -> `t.Date` 523 | - `String` -> `t.string` 524 | - `Number` -> `t.number` 525 | - `Boolean` -> `t.boolean` 526 | - any string -> The result of calling `t.exactString` with that string 527 | - any number -> The result of calling `t.exactNumber` with that number 528 | - any array -> The result of calling `t.tuple` with that array's elements as arguments 529 | - any class -> The result of calling `t.instanceOf` with that class 530 | - any object -> The result of calling `t.object` with that object (also recurses through property values and coerces them) 531 | 532 | Some things to note about Automatic `TypeDef` Coercion: 533 | 534 | - Array literals are always coerced into tuples; as such, `[String]` refers to a tuple with one element in it, a string. This may not be what you want; if you instead want an Array of any size with consistent items inside, use `t.array(String)`. 535 | 536 | To manually coerce a value into a type, you can use `t.coerceToType`: 537 | 538 | ```js 539 | t.coerceToType(Boolean); // returns t.boolean 540 | t.coerceToType("hello"); // returns t.exactString("hello") 541 | ``` 542 | 543 | ## License 544 | 545 | MIT 546 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializable-types", 3 | "version": "0.9.0", 4 | "description": "Runtime type assertion and serialization system", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/suchipi/serializable-types", 7 | "author": "Suchipi ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "types", 11 | "checking", 12 | "serialize", 13 | "serializer", 14 | "serialization", 15 | "deserialize", 16 | "deserializer", 17 | "deserialization", 18 | "json" 19 | ], 20 | "devDependencies": { 21 | "@babel/cli": "^7.0.0-beta.51", 22 | "@babel/core": "^7.0.0-beta.51", 23 | "@babel/preset-env": "^7.0.0-beta.51", 24 | "babel-core": "^7.0.0-0", 25 | "babel-eslint": "^8.2.5", 26 | "babel-loader": "^7.1.4", 27 | "eslint": "^5.0.1", 28 | "eslint-config-unobtrusive": "^1.2.2", 29 | "eslint-plugin-flowtype": "^2.49.3", 30 | "html-webpack-plugin": "^3.2.0", 31 | "prettier": "^1.13.7", 32 | "typescript": "^4.5.5", 33 | "webpack": "^4.14.0", 34 | "webpack-cli": "^3.0.8", 35 | "webpack-dev-server": "^3.1.4" 36 | }, 37 | "scripts": { 38 | "build": "rm -rf dist && babel src --out-dir dist --ignore '**/*.test.js' && cp src/index.ts dist/index.ts", 39 | "build:watch": "rm -rf dist && babel --watch src --out-dir dist", 40 | "client": "webpack-dev-server --mode development" 41 | }, 42 | "dependencies": { 43 | "pretty-format": "^23.2.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/apiFunctions.js: -------------------------------------------------------------------------------- 1 | const makeError = require("./makeError"); 2 | const { coerce } = require("./coercion"); 3 | 4 | function assertType(value, type) { 5 | const normalizedType = coerce(type); 6 | if (!normalizedType.check(value)) { 7 | throw makeError(normalizedType.description, value); 8 | } 9 | } 10 | 11 | function isOfType(value, type) { 12 | return coerce(type).check(value); 13 | } 14 | 15 | function serializeWithType(value, type) { 16 | const normalizedType = coerce(type); 17 | if (!normalizedType.check(value)) { 18 | throw makeError(normalizedType.description, value); 19 | } 20 | 21 | return normalizedType.serialize(value); 22 | } 23 | 24 | function deserializeWithType(serialized, type) { 25 | const normalizedType = coerce(type); 26 | if (!normalizedType.checkSerialized(serialized)) { 27 | throw makeError(normalizedType.serializedDescription, serialized); 28 | } 29 | 30 | return normalizedType.deserialize(serialized); 31 | } 32 | 33 | module.exports = { 34 | assertType, 35 | isOfType, 36 | serializeWithType, 37 | deserializeWithType, 38 | }; 39 | -------------------------------------------------------------------------------- /src/client-test.js: -------------------------------------------------------------------------------- 1 | const types = require("."); 2 | 3 | global.types = types; 4 | global.document.write("Open the console to try out the global `types` object"); 5 | -------------------------------------------------------------------------------- /src/coercion.js: -------------------------------------------------------------------------------- 1 | const types = require("./types"); 2 | const typeConstructors = require("./typeConstructors"); 3 | const formatValue = require("./formatValue"); 4 | 5 | const decoratedTypeDefType = typeConstructors.object({ 6 | description: types.string, 7 | serializedDescription: types.string, 8 | check: types.Function, 9 | serialize: types.Function, 10 | checkSerialized: types.Function, 11 | deserialize: types.Function, 12 | }); 13 | 14 | const coercingTypeConstructors = { 15 | array: (member) => typeConstructors.array(coerce(member)), 16 | arrayContaining: (member) => typeConstructors.arrayContaining(coerce(member)), 17 | exactNumber: typeConstructors.exactNumber, 18 | exactString: typeConstructors.exactString, 19 | func: (params, returnValue) => 20 | typeConstructors.func(params.map(coerce), coerce(returnValue)), 21 | instanceOf: typeConstructors.instanceOf, 22 | intersection: (...members) => 23 | typeConstructors.intersection(...members.map(coerce)), 24 | map: (key, value) => typeConstructors.map(coerce(key), coerce(value)), 25 | maybe: (wrapped) => typeConstructors.maybe(coerce(wrapped)), 26 | object: (defObj) => 27 | typeConstructors.object( 28 | Object.fromEntries( 29 | Object.entries(defObj).map(([key, value]) => [key, coerce(value)]) 30 | ) 31 | ), 32 | objectMap: (value, key) => 33 | typeConstructors.objectMap(coerce(value), coerce(key)), 34 | set: (member) => typeConstructors.set(coerce(member)), 35 | shape: (defObj) => 36 | typeConstructors.shape( 37 | Object.fromEntries( 38 | Object.entries(defObj).map(([key, value]) => [key, coerce(value)]) 39 | ) 40 | ), 41 | stringMatching: typeConstructors.stringMatching, 42 | symbolFor: typeConstructors.symbolFor, 43 | tuple: (...members) => typeConstructors.tuple(...members.map(coerce)), 44 | union: (...members) => typeConstructors.union(...members.map(coerce)), 45 | }; 46 | 47 | function coerce(value) { 48 | if (decoratedTypeDefType.check(value)) { 49 | return value; 50 | } else if (value === true) { 51 | return types.true; 52 | } else if (value === false) { 53 | return types.false; 54 | } else if (value === null) { 55 | return types.null; 56 | } else if (value === undefined) { 57 | return types.undefined; 58 | } else if (Number.isNaN(value)) { 59 | return types.NaN; 60 | } else if (value === Infinity) { 61 | return types.Infinity; 62 | } else if (value === -Infinity) { 63 | return types.NegativeInfinity; 64 | } else if (value === Object || value === coercingTypeConstructors.object) { 65 | return types.anyObject; 66 | } else if (typeof URL !== "undefined" && value === URL) { 67 | return types.URL; 68 | } else if (value === Symbol) { 69 | return types.Symbol; 70 | } else if (value === RegExp) { 71 | return types.RegExp; 72 | } else if (value === Function) { 73 | return types.Function; 74 | } else if (value === Error) { 75 | return types.Error; 76 | } else if (typeof Element !== "undefined" && value === Element) { 77 | return types.Element; 78 | } else if (typeof Buffer !== "undefined" && value === Buffer) { 79 | return types.Buffer; 80 | } else if (value === Date) { 81 | return types.Date; 82 | } else if (value === String) { 83 | return types.string; 84 | } else if (value === Number) { 85 | return types.number; 86 | } else if (value === Boolean) { 87 | return types.boolean; 88 | } else if (value === Array) { 89 | return typeConstructors.array(types.any); 90 | } else if (typeof value === "string") { 91 | return typeConstructors.exactString(value); 92 | } else if (typeof value === "number") { 93 | return typeConstructors.exactNumber(value); 94 | } else if (Array.isArray(value)) { 95 | return coercingTypeConstructors.tuple(...value); 96 | } else if (typeof value === "function") { 97 | return typeConstructors.instanceOf(value); 98 | } else if (value != null && typeof value === "object") { 99 | return coercingTypeConstructors.object(value); 100 | } else { 101 | throw new Error(`Unexpected value in type position: ${formatValue(value)}`); 102 | } 103 | } 104 | 105 | module.exports = { 106 | coerce, 107 | coercingTypeConstructors, 108 | }; 109 | -------------------------------------------------------------------------------- /src/formatValue.js: -------------------------------------------------------------------------------- 1 | const prettyFormat = require("pretty-format"); 2 | 3 | function formatValue(value) { 4 | return prettyFormat(value); 5 | } 6 | 7 | module.exports = formatValue; 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const types = require("./types"); 2 | const { coercingTypeConstructors, coerce } = require("./coercion"); 3 | const apiFunctions = require("./apiFunctions"); 4 | const installGlobals = require("./installGlobals"); 5 | 6 | const serializableTypes = Object.assign( 7 | { 8 | installGlobals, 9 | coerceToType: coerce, 10 | }, 11 | types, 12 | coercingTypeConstructors, 13 | apiFunctions, 14 | ); 15 | 16 | module.exports = serializableTypes; 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const dummy: any = ""; 2 | 3 | export type TypeDef = { 4 | description: string; 5 | serializedDescription: string; 6 | check(val: any): boolean; 7 | serialize(val: any): any; 8 | checkSerialized(serialized: any): boolean; 9 | deserialize(serialized: any): T; 10 | }; 11 | 12 | const global: typeof globalThis = dummy; 13 | 14 | // `TypeDef`s 15 | export const any: TypeDef = dummy; 16 | export const anyObject: TypeDef<{}> = dummy; 17 | export const boolean: TypeDef = dummy; 18 | export const Buffer: TypeDef = dummy; 19 | export const Date: TypeDef = dummy; 20 | export const Element: TypeDef = dummy; 21 | export const Error: TypeDef = dummy; 22 | export const false_: TypeDef = dummy; // `false` also exists, but TS won't let us do that 23 | export const Function: TypeDef = dummy; 24 | export const Infinity: TypeDef = dummy; 25 | export const integer: TypeDef = dummy; 26 | export const NaN: TypeDef = dummy; 27 | export const NegativeInfinity: TypeDef = dummy; 28 | export const never: TypeDef = dummy; 29 | export const null_: TypeDef = dummy; // `null` also exists, but TS won't let us do that 30 | export const number: TypeDef = dummy; 31 | export const RegExp: TypeDef = dummy; 32 | export const string: TypeDef = dummy; 33 | export const Symbol: TypeDef = dummy; 34 | export const true_: TypeDef = dummy; // `true` also exists, but TS won't let us do that 35 | export const Int8Array: TypeDef = dummy; 36 | export const Uint8Array: TypeDef = dummy; 37 | export const Uint8ClampedArray: TypeDef< 38 | typeof global.Uint8ClampedArray 39 | > = dummy; 40 | export const Int16Array: TypeDef = dummy; 41 | export const Uint16Array: TypeDef = dummy; 42 | export const Int32Array: TypeDef = dummy; 43 | export const Uint32Array: TypeDef = dummy; 44 | export const Float32Array: TypeDef = dummy; 45 | export const Float64Array: TypeDef = dummy; 46 | export const undefined: TypeDef = dummy; 47 | export const URL: TypeDef = dummy; 48 | 49 | // `TypeDef` constructors 50 | export const array = ( 51 | member: T 52 | ): TypeDef>> => dummy; 53 | 54 | export const arrayContaining = ( 55 | member: T 56 | ): TypeDef> => dummy; 57 | 58 | export const exactNumber: (num: T) => TypeDef = dummy; 59 | 60 | export const exactString: (str: T) => TypeDef = dummy; 61 | 62 | export const func: ( 63 | params: Array, 64 | returnValue: any 65 | ) => TypeDef = dummy; 66 | 67 | export const instanceOf: (klass: any) => TypeDef = dummy; 68 | 69 | export const intersection: ( 70 | ...members: Array 71 | ) => TypeDef = dummy; 72 | 73 | export const map: ( 74 | keyTypeDef: K, 75 | valueTypeDef: V 76 | ) => TypeDef, Unwrap>> = dummy; 77 | 78 | export const maybe: ( 79 | typeDef: T 80 | ) => TypeDef | undefined | null> = dummy; 81 | 82 | export const object: ( 83 | typeDefObjectMap: O 84 | ) => TypeDef<{ [K in keyof O]: Unwrap }> = dummy; 85 | 86 | interface _objectMap { 87 | (valueTypeDef: V): TypeDef< 88 | Record> 89 | >; 90 | 91 | ( 92 | valueTypeDef: V, 93 | keyTypeDef: K 94 | ): TypeDef, Unwrap>>; 95 | } 96 | export const objectMap: _objectMap = dummy; 97 | 98 | export const predicate: ( 99 | matcherFunction: (input: any) => boolean 100 | ) => TypeDef = dummy; 101 | 102 | export const set: (memberTypeDef: TypeDef) => TypeDef> = dummy; 103 | 104 | export const shape: ( 105 | typeDefObjectMap: O 106 | ) => TypeDef }>> = dummy; 107 | 108 | export const stringMatching: (regex: RegExp) => TypeDef = dummy; 109 | 110 | export const symbolFor: (tag: string) => TypeDef = dummy; 111 | 112 | export const tuple: ( 113 | ...memberTypeDefs: Array 114 | ) => TypeDef> = dummy; 115 | 116 | export const union: (...memberTypeDefs: Array) => TypeDef = dummy; 117 | 118 | // Utility functions 119 | export const assertType: ( 120 | value: any, 121 | typeDef: CoercableToTypeDef 122 | ) => void = dummy; 123 | 124 | export const isOfType: ( 125 | value: V, 126 | typeDef: T 127 | ) => value is V extends Unwrap ? Unwrap : never = dummy; 128 | 129 | export const serializeWithType: ( 130 | value: V, 131 | typeDef: DoCoerceToTypeDef extends TypeDef ? T : never 132 | ) => any = dummy; 133 | 134 | export const deserializeWithType: ( 135 | serialized: any, 136 | typeDef: T 137 | ) => Unwrap = dummy; 138 | 139 | export const installGlobals: () => void = dummy; 140 | 141 | export const coerceToType: ( 142 | value: V 143 | ) => DoCoerceToTypeDef = dummy; 144 | 145 | type UnwrapTypeDef = SomeTypeDef extends TypeDef 146 | ? U 147 | : never; 148 | 149 | export type CoercableToTypeDef = 150 | | TypeDef 151 | | true 152 | | false 153 | | null 154 | | undefined 155 | | number 156 | | (typeof object | typeof Object) 157 | | typeof global.URL 158 | | typeof global.Symbol 159 | | typeof global.RegExp 160 | | typeof global.Function 161 | | typeof global.Error 162 | | typeof global.Element 163 | | typeof global.Buffer 164 | | typeof global.Date 165 | | typeof global.String 166 | | typeof global.Number 167 | | typeof global.Boolean 168 | | typeof global.Array 169 | | string 170 | | number 171 | | Array 172 | | Function 173 | | {}; 174 | 175 | type DoCoerceToTypeDef = 176 | // prettier-ignore 177 | V extends TypeDef ? V : 178 | V extends true ? typeof true_ : 179 | V extends false ? typeof false_ : 180 | V extends null ? typeof null_ : 181 | V extends undefined ? TypeDef : 182 | V extends number ? TypeDef : 183 | V extends (typeof object | typeof Object) ? typeof anyObject : 184 | V extends typeof global.URL ? typeof URL : 185 | V extends typeof global.Symbol ? typeof Symbol : 186 | V extends typeof global.RegExp ? typeof RegExp : 187 | V extends typeof global.Function ? typeof Function : 188 | V extends typeof global.Error ? typeof Error : 189 | V extends typeof global.Element ? typeof Element : 190 | V extends typeof global.Buffer ? typeof Buffer : 191 | V extends typeof global.Date ? typeof Date : 192 | V extends typeof global.String ? typeof string : 193 | V extends typeof global.Number ? typeof number : 194 | V extends typeof global.Boolean ? typeof boolean : 195 | V extends typeof global.Array ? TypeDef> : 196 | V extends string ? TypeDef : // exactString 197 | V extends number ? TypeDef : // exactNumber 198 | V extends Array ? TypeDef> : // tuple 199 | V extends Function ? TypeDef : 200 | V extends {} ? TypeDef<{ [K in keyof V]: DoCoerceToTypeDef }> : 201 | never; 202 | 203 | export type Unwrap = UnwrapTypeDef< 204 | DoCoerceToTypeDef 205 | >; 206 | -------------------------------------------------------------------------------- /src/installGlobals.js: -------------------------------------------------------------------------------- 1 | const types = require("./types"); 2 | const { coercingTypeConstructors } = require("./coercion"); 3 | const apiFunctions = require("./apiFunctions"); 4 | 5 | function installGlobals() { 6 | Object.assign(global, apiFunctions, coercingTypeConstructors, { 7 | boolean: types.boolean, 8 | integer: types.integer, 9 | number: types.number, 10 | string: types.string, 11 | never: types.never, 12 | }); 13 | } 14 | 15 | module.exports = installGlobals; 16 | -------------------------------------------------------------------------------- /src/makeError.js: -------------------------------------------------------------------------------- 1 | const formatValue = require("./formatValue"); 2 | 3 | const makeError = function makeError(description, value) { 4 | return new TypeError( 5 | `Expected ${description}, but received ${formatValue(value)}` 6 | ); 7 | }; 8 | 9 | module.exports = makeError; 10 | -------------------------------------------------------------------------------- /src/typeConstructors/array.js: -------------------------------------------------------------------------------- 1 | module.exports = function array(memberDef) { 2 | return { 3 | description: `Array<${memberDef.description}>`, 4 | serializedDescription: `{ $type: "array", $value: Array<${ 5 | memberDef.serializedDescription 6 | }> }`, 7 | 8 | check(val) { 9 | return ( 10 | Array.isArray(val) && val.every((member) => memberDef.check(member)) 11 | ); 12 | }, 13 | 14 | serialize(array) { 15 | return { 16 | $type: "array", 17 | $value: array.map((member) => memberDef.serialize(member)), 18 | }; 19 | }, 20 | 21 | checkSerialized(serialized) { 22 | return ( 23 | serialized.$type === "array" && 24 | serialized.$value.every((serializedMember) => 25 | memberDef.checkSerialized(serializedMember) 26 | ) 27 | ); 28 | }, 29 | 30 | deserialize(serialized) { 31 | return serialized.$value.map((serializedValue) => { 32 | return memberDef.deserialize(serializedValue); 33 | }); 34 | }, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/typeConstructors/arrayContaining.js: -------------------------------------------------------------------------------- 1 | module.exports = function arrayContaining(memberDef) { 2 | return { 3 | description: `Array containing at least one ${memberDef.description}`, 4 | serializedDescription: `{ $type: "array", $value: Array containing at least one ${ 5 | memberDef.serializedDescription 6 | } }`, 7 | 8 | check(val) { 9 | return ( 10 | Array.isArray(val) && val.some((member) => memberDef.check(member)) 11 | ); 12 | }, 13 | 14 | serialize(array) { 15 | return { 16 | $type: "array", 17 | $value: array.map((member) => memberDef.serialize(member)), 18 | }; 19 | }, 20 | 21 | checkSerialized(serialized) { 22 | return ( 23 | serialized.$type === "array" && 24 | serialized.$value.some((serializedMember) => 25 | memberDef.checkSerialized(serializedMember) 26 | ) 27 | ); 28 | }, 29 | 30 | deserialize(serialized) { 31 | return serialized.$value.map((serializedValue) => { 32 | return memberDef.deserialize(serializedValue); 33 | }); 34 | }, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/typeConstructors/exactNumber.js: -------------------------------------------------------------------------------- 1 | module.exports = function exactNumber(num) { 2 | return { 3 | description: JSON.stringify(num), 4 | serializedDescription: `{ $type: "number", $value: ${JSON.stringify( 5 | num 6 | )} }`, 7 | 8 | check(val) { 9 | return val === num; 10 | }, 11 | 12 | serialize(val) { 13 | return { 14 | $type: "number", 15 | $value: val, 16 | }; 17 | }, 18 | 19 | checkSerialized(serialized) { 20 | return serialized.$type === "number" && serialized.$value === num; 21 | }, 22 | 23 | deserialize(serialized) { 24 | return serialized.$value; 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/typeConstructors/exactString.js: -------------------------------------------------------------------------------- 1 | module.exports = function exactString(str) { 2 | return { 3 | description: JSON.stringify(str), 4 | serializedDescription: `{ $type: "string", $value: ${JSON.stringify( 5 | str 6 | )} }`, 7 | 8 | check(val) { 9 | return val === str; 10 | }, 11 | 12 | serialize(val) { 13 | return { 14 | $type: "string", 15 | $value: val, 16 | }; 17 | }, 18 | 19 | checkSerialized(serialized) { 20 | return serialized.$type === "string" && serialized.$value === str; 21 | }, 22 | 23 | deserialize(serialized) { 24 | return serialized.$value; 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/typeConstructors/func.js: -------------------------------------------------------------------------------- 1 | module.exports = function func(params, returnValue) { 2 | return { 3 | description: "Function", 4 | serializedDescription: "Functions cannot be serialized", 5 | 6 | check(val) { 7 | // Can't actually validate the params of a function without decorating every function that 8 | // could come through here, which isn't practical at runtime. So this type is mostly just 9 | // for documentation purposes, and the runtime treats it the same as types.Function. 10 | return typeof val === "function"; 11 | }, 12 | 13 | serialize(buffer) { 14 | throw new TypeError("Functions cannot be serialized"); 15 | }, 16 | 17 | checkSerialized(serialized) { 18 | return false; 19 | }, 20 | 21 | deserialize(serialized) { 22 | throw new TypeError("Functions cannot be deserialized"); 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/typeConstructors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | array: require("./array"), 3 | arrayContaining: require("./arrayContaining"), 4 | exactNumber: require("./exactNumber"), 5 | exactString: require("./exactString"), 6 | func: require("./func"), 7 | instanceOf: require("./instanceOf"), 8 | intersection: require("./intersection"), 9 | map: require("./map"), 10 | maybe: require("./maybe"), 11 | object: require("./object"), 12 | objectMap: require("./objectMap"), 13 | predicate: require("./predicate"), 14 | set: require("./set"), 15 | shape: require("./shape"), 16 | stringMatching: require("./stringMatching"), 17 | symbolFor: require("./symbolFor"), 18 | tuple: require("./tuple"), 19 | union: require("./union"), 20 | }; 21 | -------------------------------------------------------------------------------- /src/typeConstructors/instanceOf.js: -------------------------------------------------------------------------------- 1 | module.exports = function instanceOf(klass) { 2 | return { 3 | description: `${klass.name}`, 4 | serializedDescription: `Serialized ${klass.name}`, 5 | 6 | check(val) { 7 | return val instanceof klass; 8 | }, 9 | 10 | serialize(val) { 11 | if (klass.serialize) { 12 | return klass.serialize(val); 13 | } else { 14 | throw new Error( 15 | "Cannot serialize class instances unless their class has a static 'serialize' method." 16 | ); 17 | } 18 | }, 19 | 20 | checkSerialized(serialized) { 21 | if (klass.checkSerialized) { 22 | return klass.checkSerialized(serialized); 23 | } else { 24 | throw new Error( 25 | "Cannot use checkSerialized with class unless the class has a static 'checkSerialized' method." 26 | ); 27 | } 28 | }, 29 | 30 | deserialize(serialized) { 31 | if (klass.deserialize) { 32 | return klass.deserialize(serialized); 33 | } else { 34 | throw new Error( 35 | "Cannot deserialize class instances unless their class has a static 'deserialize' method." 36 | ); 37 | } 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/typeConstructors/intersection.js: -------------------------------------------------------------------------------- 1 | module.exports = function intersection(...typeDefs) { 2 | return { 3 | description: typeDefs.map((typeDef) => typeDef.description).join(" & "), 4 | serializedDescription: `{ $type: "intersection", $value: [ ${typeDefs 5 | .map((typeDef) => typeDef.serializedDescription) 6 | .join(", ")} ] }`, 7 | 8 | check(val) { 9 | return typeDefs.every((typeDef) => typeDef.check(val)); 10 | }, 11 | 12 | serialize(val) { 13 | return { 14 | $type: "intersection", 15 | $value: typeDefs.map((typeDef) => typeDef.serialize(val)), 16 | }; 17 | }, 18 | 19 | checkSerialized(serialized) { 20 | return ( 21 | serialized.$type === "intersection" && 22 | typeDefs.every((typeDef, index) => 23 | typeDef.checkSerialized(serialized.$value[index]) 24 | ) 25 | ); 26 | }, 27 | 28 | deserialize(serialized) { 29 | let val; 30 | typeDefs.forEach((typeDef, index) => { 31 | if (typeof val === "undefined") { 32 | val = typeDef.deserialize(serialized.$value[index]); 33 | } else { 34 | Object.assign(val, typeDef.deserialize(serialized.$value[index])); 35 | } 36 | }); 37 | return val; 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/typeConstructors/map.js: -------------------------------------------------------------------------------- 1 | module.exports = function map(keyDef, valueDef) { 2 | return { 3 | description: `Map<${keyDef.description}, ${valueDef.description}>`, 4 | serializedDescription: `{ $type: "Map", $value: Array<[${ 5 | keyDef.serializedDescription 6 | }, ${valueDef.serializedDescription}]> }`, 7 | 8 | check(val) { 9 | return ( 10 | val instanceof Map && 11 | Array.from(val).every( 12 | ([key, value]) => keyDef.check(key) && valueDef.check(value) 13 | ) 14 | ); 15 | }, 16 | 17 | serialize(map) { 18 | return { 19 | $type: "Map", 20 | $value: Array.from(map).map(([key, val]) => [ 21 | keyDef.serialize(key), 22 | valueDef.serialize(val), 23 | ]), 24 | }; 25 | }, 26 | 27 | checkSerialized(serialized) { 28 | return ( 29 | serialized.$type === "Map" && 30 | serialized.$value.every( 31 | ([serializedKey, serializedVal]) => 32 | keyDef.checkSerialized(serializedKey) && 33 | valueDef.checkSerialized(serializedVal) 34 | ) 35 | ); 36 | }, 37 | 38 | deserialize(serialized) { 39 | const map = new Map(); 40 | serialized.$value.forEach(([serializedKey, serializedValue]) => { 41 | map.set( 42 | keyDef.deserialize(serializedKey), 43 | valueDef.deserialize(serializedValue) 44 | ); 45 | }); 46 | return map; 47 | }, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/typeConstructors/maybe.js: -------------------------------------------------------------------------------- 1 | const undefinedDef = require("../types/undefined"); 2 | 3 | const union = require("./union"); 4 | 5 | module.exports = function maybe(typeDef) { 6 | return union(typeDef, undefinedDef); 7 | }; 8 | -------------------------------------------------------------------------------- /src/typeConstructors/object.js: -------------------------------------------------------------------------------- 1 | module.exports = function object(objectDef) { 2 | const entries = Object.entries(objectDef); 3 | return { 4 | description: `{ ${entries 5 | .map(([key, typeDef]) => `${key}: ${typeDef.description}`) 6 | .join(", ")} }`, 7 | serializedDescription: `{ $type: "object", $value: { ${entries 8 | .map(([key, typeDef]) => `${key}: ${typeDef.serializedDescription}`) 9 | .join(", ")} } }`, 10 | 11 | check(val) { 12 | return ( 13 | val != null && 14 | typeof val === "object" && 15 | entries.every(([key, typeDef]) => typeDef.check(val[key])) 16 | ); 17 | }, 18 | 19 | serialize(obj) { 20 | const value = {}; 21 | entries.forEach(([key, typeDef]) => { 22 | value[key] = typeDef.serialize(obj[key]); 23 | }); 24 | return { 25 | $type: "object", 26 | $value: value, 27 | }; 28 | }, 29 | 30 | checkSerialized(serialized) { 31 | if (serialized.$type !== "object") { 32 | return false; 33 | } 34 | 35 | const serializedEntries = Object.entries(serialized.$value); 36 | return serializedEntries.every( 37 | ([key, serializedValue]) => 38 | objectDef[key] && objectDef[key].checkSerialized(serializedValue) 39 | ); 40 | }, 41 | 42 | deserialize(serialized) { 43 | const value = {}; 44 | entries.forEach(([key, typeDef]) => { 45 | value[key] = typeDef.deserialize(serialized.$value[key]); 46 | }); 47 | return value; 48 | }, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/typeConstructors/objectMap.js: -------------------------------------------------------------------------------- 1 | const union = require("./union"); 2 | const stringDef = require("../types/string"); 3 | const symbolDef = require("../types/Symbol"); 4 | 5 | module.exports = function objectMap( 6 | valueType, 7 | keyType = union(stringDef, symbolDef) 8 | ) { 9 | return { 10 | description: `{ [${keyType.description}]: ${valueType.description} }`, 11 | serializedDescription: `{ $type: "objectMap", $value: Array<[${ 12 | keyType.description 13 | }, ${valueType.description}]> }`, 14 | 15 | check(val) { 16 | return ( 17 | val != null && 18 | typeof val === "object" && 19 | Object.entries(val).every( 20 | ([key, val]) => keyType.check(key) && valueType.check(val) 21 | ) 22 | ); 23 | }, 24 | 25 | serialize(obj) { 26 | return { 27 | $type: "objectMap", 28 | $value: Object.entries(obj).map(([key, val]) => { 29 | return [keyType.serialize(key), valueType.serialize(val)]; 30 | }), 31 | }; 32 | }, 33 | 34 | checkSerialized(serialized) { 35 | if (serialized.$type !== "objectMap") { 36 | return false; 37 | } 38 | 39 | return serialized.$value.every( 40 | ([key, val]) => 41 | keyType.checkSerialized(key) && valueType.checkSerialized(val) 42 | ); 43 | }, 44 | 45 | deserialize(serialized) { 46 | const obj = {}; 47 | serialized.$value.forEach(([serializedKey, serializedValue]) => { 48 | const key = keyType.deserialize(serializedKey); 49 | const val = valueType.deserialize(serializedValue); 50 | obj[key] = val; 51 | }); 52 | return obj; 53 | }, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/typeConstructors/predicate.js: -------------------------------------------------------------------------------- 1 | module.exports = function predicate(matcherFunction) { 2 | return { 3 | description: `Predicate type for: ${matcherFunction.toString()}`, 4 | serializedDescription: `Predicate types cannot be serialized`, 5 | 6 | check(val) { 7 | return matcherFunction(val); 8 | }, 9 | 10 | serialize(array) { 11 | throw new Error("Predicate types cannot be serialized"); 12 | }, 13 | 14 | checkSerialized(serialized) { 15 | throw new Error("Predicate types cannot be serialized"); 16 | }, 17 | 18 | deserialize(serialized) { 19 | throw new Error("Predicate types cannot be deserialized"); 20 | }, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/typeConstructors/set.js: -------------------------------------------------------------------------------- 1 | module.exports = function set(memberDef) { 2 | return { 3 | description: `Set<${memberDef.description}>`, 4 | serializedDescription: `{ $type: "Set", $value: Array<${ 5 | memberDef.serializedDescription 6 | }> }`, 7 | 8 | check(val) { 9 | return ( 10 | val instanceof Set && 11 | Array.from(val).every((member) => memberDef.check(member)) 12 | ); 13 | }, 14 | 15 | serialize(set) { 16 | return { 17 | $type: "Set", 18 | $value: Array.from(set).map((member) => memberDef.serialize(member)), 19 | }; 20 | }, 21 | 22 | checkSerialized(serialized) { 23 | return ( 24 | serialized.$type === "Set" && 25 | serialized.$value.every((serializedMember) => 26 | memberDef.checkSerialized(serializedMember) 27 | ) 28 | ); 29 | }, 30 | 31 | deserialize(serialized) { 32 | return new Set( 33 | serialized.$value.map((serializedValue) => { 34 | return memberDef.deserialize(serializedValue); 35 | }) 36 | ); 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/typeConstructors/shape.js: -------------------------------------------------------------------------------- 1 | const object = require("./object"); 2 | 3 | const maybe = require("./maybe"); 4 | 5 | module.exports = function shape(objectDef) { 6 | const shapeDef = {}; 7 | const entries = Object.entries(objectDef); 8 | entries.forEach(([key, typeDef]) => { 9 | shapeDef[key] = maybe(typeDef); 10 | }); 11 | return object(shapeDef); 12 | }; 13 | -------------------------------------------------------------------------------- /src/typeConstructors/stringMatching.js: -------------------------------------------------------------------------------- 1 | module.exports = function stringMatching(regex) { 2 | return { 3 | description: `string matching ${regex.toString()}`, 4 | serializedDescription: `{ $type: "string", $value: string matching ${regex.toString()} }`, 5 | 6 | check(val) { 7 | return regex.test(val); 8 | }, 9 | 10 | serialize(val) { 11 | return { 12 | $type: "string", 13 | $value: val, 14 | }; 15 | }, 16 | 17 | checkSerialized(serialized) { 18 | return serialized.$type === "string" && regex.test(serialized.$value); 19 | }, 20 | 21 | deserialize(serialized) { 22 | return serialized.$value; 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/typeConstructors/symbolFor.js: -------------------------------------------------------------------------------- 1 | module.exports = function symbolFor(tag) { 2 | return { 3 | description: `Symbol.for(${JSON.stringify(tag)})`, 4 | serializedDescription: `{ $type: "SymbolFor", $value: ${JSON.stringify( 5 | tag 6 | )} }`, 7 | 8 | check(val) { 9 | return val === Symbol.for(tag); 10 | }, 11 | 12 | serialize(val) { 13 | return { 14 | $type: "SymbolFor", 15 | $value: tag, 16 | }; 17 | }, 18 | 19 | checkSerialized(serialized) { 20 | return serialized.$type === "SymbolFor" && serialized.$value === tag; 21 | }, 22 | 23 | deserialize(serialized) { 24 | return Symbol.for(tag); 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/typeConstructors/tuple.js: -------------------------------------------------------------------------------- 1 | module.exports = function tuple(...memberDefs) { 2 | return { 3 | description: `[${memberDefs 4 | .map((typeDef) => typeDef.description) 5 | .join(", ")}]`, 6 | serializedDescription: `[${memberDefs 7 | .map((typeDef) => typeDef.description) 8 | .join(", ")}]`, 9 | 10 | check(val) { 11 | return ( 12 | val != null && 13 | typeof val === "object" && 14 | memberDefs.length === val.length && 15 | memberDefs.every((typeDef, index) => typeDef.check(val[index])) 16 | ); 17 | }, 18 | 19 | serialize(tuple) { 20 | return { 21 | $type: "tuple", 22 | $value: Array.from(tuple).map((member, index) => 23 | memberDefs[index].serialize(member) 24 | ), 25 | }; 26 | }, 27 | 28 | checkSerialized(serialized) { 29 | return ( 30 | serialized.$type === "tuple" && 31 | serialized.$value.length === memberDefs.length && 32 | serialized.$value.every((serializedValue, index) => { 33 | const typeDef = memberDefs[index]; 34 | return typeDef.checkSerialized(serializedValue); 35 | }) 36 | ); 37 | }, 38 | 39 | deserialize(serialized) { 40 | return serialized.$value.map((serializedValue, index) => { 41 | const typeDef = memberDefs[index]; 42 | return typeDef.deserialize(serializedValue); 43 | }); 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/typeConstructors/tuple.test.js: -------------------------------------------------------------------------------- 1 | const types = require("../.."); 2 | 3 | test("tuple", () => { 4 | const t = types.tuple(types.number, types.string); 5 | expect(t.check([1])).toBe(false); 6 | expect(t.check([1, "hi"])).toBe(true); 7 | expect(t.check([1, 1])).toBe(false); 8 | expect(() => { 9 | types.serializeWithType([1], t); 10 | }).toThrowErrorMatchingInlineSnapshot(` 11 | "Expected [number, string], but received Array [ 12 | 1, 13 | ]" 14 | `); 15 | }); 16 | test("union of tuples", () => { 17 | const t = types.union( 18 | types.tuple(types.number), 19 | types.tuple(types.number, types.maybe(types.number)) 20 | ); 21 | expect(t.serialize([1])).toMatchInlineSnapshot(` 22 | Object { 23 | "$type": "tuple", 24 | "$value": Array [ 25 | Object { 26 | "$type": "number", 27 | "$value": 1, 28 | }, 29 | ], 30 | } 31 | `); 32 | expect(t.serialize([1, 2])).toMatchInlineSnapshot(` 33 | Object { 34 | "$type": "tuple", 35 | "$value": Array [ 36 | Object { 37 | "$type": "number", 38 | "$value": 1, 39 | }, 40 | Object { 41 | "$type": "number", 42 | "$value": 2, 43 | }, 44 | ], 45 | } 46 | `); 47 | expect(t.serialize([1, undefined])).toMatchInlineSnapshot(` 48 | Object { 49 | "$type": "tuple", 50 | "$value": Array [ 51 | Object { 52 | "$type": "number", 53 | "$value": 1, 54 | }, 55 | Object { 56 | "$type": "undefined", 57 | "$value": undefined, 58 | }, 59 | ], 60 | } 61 | `); 62 | expect(() => t.deserialize(t.serialize([1]))).not.toThrowError(); 63 | expect(() => t.deserialize(t.serialize([1, 2]))).not.toThrowError(); 64 | expect(() => t.deserialize(t.serialize([1, undefined]))).not.toThrowError(); 65 | }); 66 | -------------------------------------------------------------------------------- /src/typeConstructors/union.js: -------------------------------------------------------------------------------- 1 | const makeError = require("../makeError"); 2 | module.exports = function union(...typeDefs) { 3 | return { 4 | description: typeDefs.map((typeDef) => typeDef.description).join(" | "), 5 | serializedDescription: typeDefs 6 | .map((typeDef) => typeDef.serializedDescription) 7 | .join(" | "), 8 | 9 | check(val) { 10 | return typeDefs.some((typeDef) => typeDef.check(val)); 11 | }, 12 | 13 | serialize(val) { 14 | const defToSerializeWith = typeDefs.find((typeDef) => typeDef.check(val)); 15 | 16 | if (defToSerializeWith == null) { 17 | throw makeError(this.description, val); 18 | } 19 | 20 | return defToSerializeWith.serialize(val); 21 | }, 22 | 23 | checkSerialized(serialized) { 24 | return typeDefs.some((typeDef) => typeDef.checkSerialized(serialized)); 25 | }, 26 | 27 | deserialize(serialized) { 28 | const defToDeserializeWith = typeDefs.find((typeDef) => 29 | typeDef.checkSerialized(serialized) 30 | ); 31 | 32 | if (defToDeserializeWith == null) { 33 | throw makeError(this.serializedDescription, serialized); 34 | } 35 | 36 | return defToDeserializeWith.deserialize(serialized); 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/types/Buffer.js: -------------------------------------------------------------------------------- 1 | const Buffer = require("buffer").Buffer; 2 | 3 | module.exports = { 4 | description: "Buffer", 5 | serializedDescription: '{ $type: "Buffer", $value: Array }', 6 | 7 | check(val) { 8 | return Buffer.isBuffer(val); 9 | }, 10 | 11 | serialize(buffer) { 12 | return { 13 | $type: "Buffer", 14 | $value: Array.from(buffer.values()), 15 | }; 16 | }, 17 | 18 | checkSerialized(serialized) { 19 | return serialized.$type === "Buffer"; 20 | }, 21 | 22 | deserialize(serialized) { 23 | return Buffer.from(serialized.$value); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/types/Buffer.test.js: -------------------------------------------------------------------------------- 1 | const typeDef = require("./Buffer"); 2 | 3 | test("Buffer", () => { 4 | const b = Buffer.from("abc"); 5 | 6 | expect(typeDef.check(b)).toBe(true); 7 | expect(typeDef.check("not a buffer")).toBe(false); 8 | 9 | expect(typeDef.serialize(b)).toEqual({ 10 | $type: "Buffer", 11 | $value: [97, 98, 99], 12 | }); 13 | 14 | expect( 15 | typeDef.checkSerialized({ 16 | $type: "Buffer", 17 | $value: [97, 98, 99], 18 | }) 19 | ).toBe(true); 20 | 21 | const b2 = typeDef.deserialize({ 22 | $type: "Buffer", 23 | $value: [97, 98, 99], 24 | }); 25 | expect(Buffer.isBuffer(b2)).toBe(true); 26 | expect(b2.toString()).toBe("abc"); 27 | }); 28 | -------------------------------------------------------------------------------- /src/types/Date.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Date", 3 | serializedDescription: '{ $type: "Date", $value: number }', 4 | 5 | check(val) { 6 | return val instanceof Date; 7 | }, 8 | 9 | serialize(date) { 10 | return { 11 | $type: "Date", 12 | $value: Number(date), 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "Date"; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return new Date(serialized.$value); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/Element.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Element | TextNode", 3 | serializedDescription: 4 | '{ $type: "TextNode", $value: string } | { $type: "Element", $value: { element: { tagName: string, namespaceURI: ?string }, attributes: Array<{ namespaceURI: ?string, name: string, value: string }>, children: Array } }', 5 | 6 | check(val) { 7 | return ( 8 | val instanceof global.Node && (val.nodeType === 1 || val.nodeType === 3) 9 | ); 10 | }, 11 | 12 | serialize(node) { 13 | if (node instanceof Element) { 14 | return { 15 | $type: "Element", 16 | $value: { 17 | element: { 18 | tagName: node.tagName, 19 | namespaceURI: node.namespaceURI, 20 | }, 21 | attributes: Array.from(node.attributes).map( 22 | ({ namespaceURI, name, value }) => ({ 23 | namespaceURI, 24 | name, 25 | value, 26 | }) 27 | ), 28 | children: Array.from(node.childNodes).map((child) => 29 | this.serialize(child) 30 | ), 31 | }, 32 | }; 33 | } else { 34 | return { 35 | $type: "TextNode", 36 | $value: node.nodeValue, 37 | }; 38 | } 39 | }, 40 | 41 | checkSerialized(serialized) { 42 | return serialized.$type === "Element" || serialized.$type === "Node"; 43 | }, 44 | 45 | deserialize(serialized) { 46 | const document = global.document; 47 | 48 | if (document == null) { 49 | throw new Error( 50 | "Cannot deserialize an Element without a global document in scope" 51 | ); 52 | } 53 | 54 | if (typeof serialized.$value === "string") { 55 | return document.createTextNode(serialized.$value); 56 | } else { 57 | const { element, attributes, children } = serialized.$value; 58 | let el; 59 | 60 | if (element.namespaceURI != null) { 61 | el = document.createElementNS(element.namespaceURI, element.tagName); 62 | } else { 63 | el = document.createElement(element.tagName); 64 | } 65 | 66 | attributes.forEach(({ namespaceURI, name, value }) => { 67 | if (namespaceURI != null) { 68 | el.setAttributeNS(namespaceURI, name, value); 69 | } else { 70 | el.setAttribute(name, value); 71 | } 72 | }); 73 | children.forEach((child) => el.appendChild(this.deserialize(child))); 74 | return el; 75 | } 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/types/Error.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Error", 3 | serializedDescription: 4 | '{ $type: "Error", $value: { name: string, message: string, stack: string } }', 5 | 6 | check(val) { 7 | return val instanceof Error; 8 | }, 9 | 10 | serialize(error) { 11 | return { 12 | $type: "Error", 13 | $value: { 14 | name: error.name, 15 | message: error.message, 16 | stack: error.stack, 17 | }, 18 | }; 19 | }, 20 | 21 | checkSerialized(serialized) { 22 | return serialized.$type === "Error"; 23 | }, 24 | 25 | deserialize(serialized) { 26 | const error = new Error(serialized.$value.message); 27 | Object.defineProperty(error, "name", { 28 | value: serialized.$value.name, 29 | }); 30 | Object.defineProperty(error, "stack", { 31 | value: serialized.$value.stack, 32 | }); 33 | return error; 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/types/Function.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Function", 3 | serializedDescription: "Functions cannot be serialized", 4 | 5 | check(val) { 6 | return typeof val === "function"; 7 | }, 8 | 9 | serialize(buffer) { 10 | throw new TypeError("Functions cannot be serialized"); 11 | }, 12 | 13 | checkSerialized(serialized) { 14 | return false; 15 | }, 16 | 17 | deserialize(serialized) { 18 | throw new TypeError("Functions cannot be deserialized"); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/types/Infinity.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Infinity", 3 | serializedDescription: '{ $type: "Infinity" }', 4 | 5 | check(val) { 6 | return val === Infinity; 7 | }, 8 | 9 | serialize(number) { 10 | return { 11 | $type: "Infinity", 12 | }; 13 | }, 14 | 15 | checkSerialized(serialized) { 16 | return serialized.$type === "Infinity"; 17 | }, 18 | 19 | deserialize(serialized) { 20 | return Infinity; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/NaN.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "NaN", 3 | serializedDescription: '{ $type: "NaN", $value: NaN }', 4 | 5 | check(val) { 6 | return Number.isNaN(val); 7 | }, 8 | 9 | serialize(number) { 10 | return { 11 | $type: "NaN", 12 | $value: NaN, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "NaN"; 18 | }, 19 | 20 | deserialize() { 21 | return NaN; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/NegativeInfinity.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "-Infinity", 3 | serializedDescription: '{ $type: "-Infinity" }', 4 | 5 | check(val) { 6 | return val === -Infinity; 7 | }, 8 | 9 | serialize(number) { 10 | return { 11 | $type: "-Infinity", 12 | }; 13 | }, 14 | 15 | checkSerialized(serialized) { 16 | return serialized.$type === "-Infinity"; 17 | }, 18 | 19 | deserialize(serialized) { 20 | return -Infinity; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/RegExp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "RegExp", 3 | serializedDescription: 4 | '{ $type: "RegExp", $value: { pattern: string, flags: string } }', 5 | 6 | check(val) { 7 | return val instanceof RegExp; 8 | }, 9 | 10 | serialize(regexp) { 11 | return { 12 | $type: "RegExp", 13 | $value: { 14 | pattern: regexp.source, 15 | flags: regexp.flags, 16 | }, 17 | }; 18 | }, 19 | 20 | checkSerialized(serialized) { 21 | return serialized.$type === "RegExp"; 22 | }, 23 | 24 | deserialize(serialized) { 25 | return new RegExp(serialized.$value.pattern, serialized.$value.flags); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/types/Symbol.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "Symbol", 3 | serializedDescription: '{ $type: "Symbol", $value: string }', 4 | 5 | check(val) { 6 | return typeof val === "symbol"; 7 | }, 8 | 9 | serialize(val) { 10 | const key = Symbol.keyFor(val); 11 | 12 | if (key == null) { 13 | throw new Error( 14 | "Can only serialize symbols from the global symbol registry." 15 | ); 16 | } 17 | 18 | return { 19 | $type: "Symbol", 20 | $value: key, 21 | }; 22 | }, 23 | 24 | checkSerialized(serialized) { 25 | return serialized.$type === "Symbol"; 26 | }, 27 | 28 | deserialize(serialized) { 29 | return Symbol.for(serialized.$value); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/types/URL.js: -------------------------------------------------------------------------------- 1 | // Use eval("require") so webpack doesn't bundle the `url` module 2 | const URL = global.URL || eval("require")("url").URL; 3 | 4 | module.exports = { 5 | description: "URL", 6 | serializedDescription: '{ $type: "URL", $value: string }', 7 | 8 | check(val) { 9 | return val instanceof URL; 10 | }, 11 | 12 | serialize(url) { 13 | return { 14 | $type: "URL", 15 | $value: url.href, 16 | }; 17 | }, 18 | 19 | checkSerialized(serialized) { 20 | return serialized.$type === "URL"; 21 | }, 22 | 23 | deserialize(serialized) { 24 | return new URL(serialized.$value); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/types/any.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "any", 3 | serializedDescription: "Values of type 'any' cannot be serialized", 4 | 5 | check(_val) { 6 | return true; 7 | }, 8 | 9 | serialize(_val) { 10 | throw new Error("Values of type 'any' cannot be serialized"); 11 | }, 12 | 13 | checkSerialized(_serialized) { 14 | return false; 15 | }, 16 | 17 | deserialize(_serialized) { 18 | throw new Error("Values of type 'any' cannot be serialized"); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/types/anyObject.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "any Object", 3 | serializedDescription: '{ $type: "object", $value: Object }', 4 | 5 | check(val) { 6 | return val != null && typeof val === "object"; 7 | }, 8 | 9 | serialize(val) { 10 | return { 11 | $type: "object", 12 | $value: val, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "object"; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/boolean.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "boolean", 3 | serializedDescription: '{ $type: "boolean", $value: boolean }', 4 | 5 | check(val) { 6 | return typeof val === "boolean"; 7 | }, 8 | 9 | serialize(bool) { 10 | return { 11 | $type: "boolean", 12 | $value: bool, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "boolean"; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/boolean.test.js: -------------------------------------------------------------------------------- 1 | const typeDef = require("./boolean"); 2 | 3 | test("boolean", () => { 4 | expect(typeDef.check(true)).toBe(true); 5 | expect(typeDef.check(false)).toBe(true); 6 | 7 | expect(typeDef.check(42)).toBe(false); 8 | 9 | expect(typeDef.serialize(true)).toEqual({ 10 | $type: "boolean", 11 | $value: true, 12 | }); 13 | 14 | expect( 15 | typeDef.checkSerialized({ 16 | $type: "boolean", 17 | $value: true, 18 | }) 19 | ).toBe(true); 20 | 21 | expect( 22 | typeDef.deserialize({ 23 | $type: "boolean", 24 | $value: false, 25 | }) 26 | ).toBe(false); 27 | }); 28 | -------------------------------------------------------------------------------- /src/types/false.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "false", 3 | serializedDescription: '{ $type: "boolean", $value: false }', 4 | 5 | check(val) { 6 | return val === false; 7 | }, 8 | 9 | serialize(bool) { 10 | return { 11 | $type: "boolean", 12 | $value: bool, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "boolean" && serialized.$value === false; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | const makeTypedArrayDef = require("./makeTypedArrayDef"); 2 | 3 | module.exports = { 4 | any: require("./any"), 5 | anyObject: require("./anyObject"), 6 | boolean: require("./boolean"), 7 | Buffer: require("./Buffer"), 8 | Date: require("./Date"), 9 | Element: require("./Element"), 10 | Error: require("./Error"), 11 | false: require("./false"), 12 | false_: require("./false"), // for TS 13 | Function: require("./Function"), 14 | Infinity: require("./Infinity"), 15 | integer: require("./integer"), 16 | NaN: require("./NaN"), 17 | NegativeInfinity: require("./NegativeInfinity"), 18 | never: require("./never"), 19 | null: require("./null"), 20 | null_: require("./null"), // for TS 21 | number: require("./number"), 22 | RegExp: require("./RegExp"), 23 | string: require("./string"), 24 | Symbol: require("./Symbol"), 25 | true: require("./true"), 26 | true_: require("./true"), // for TS 27 | Int8Array: makeTypedArrayDef(Int8Array), 28 | Uint8Array: makeTypedArrayDef(Uint8Array), 29 | Uint8ClampedArray: makeTypedArrayDef(Uint8ClampedArray), 30 | Int16Array: makeTypedArrayDef(Int16Array), 31 | Uint16Array: makeTypedArrayDef(Uint16Array), 32 | Int32Array: makeTypedArrayDef(Int32Array), 33 | Uint32Array: makeTypedArrayDef(Uint32Array), 34 | Float32Array: makeTypedArrayDef(Float32Array), 35 | Float64Array: makeTypedArrayDef(Float64Array), 36 | undefined: require("./undefined"), 37 | URL: require("./URL"), 38 | }; 39 | -------------------------------------------------------------------------------- /src/types/integer.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "integer", 3 | serializedDescription: '{ $type: "integer", $value: number }', 4 | 5 | check(val) { 6 | return typeof val === "number" && parseInt(String(val)) === val; 7 | }, 8 | 9 | serialize(integer) { 10 | return { 11 | $type: "integer", 12 | $value: integer, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "integer"; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/makeTypedArrayDef.js: -------------------------------------------------------------------------------- 1 | module.exports = function makeTypedArrayDef(klass) { 2 | return { 3 | description: klass.name, 4 | serializedDescription: `{ $type: "${klass.name}", $value: Array }`, 5 | 6 | check(val) { 7 | return val instanceof klass; 8 | }, 9 | 10 | serialize(typedArray) { 11 | return { 12 | $type: klass.name, 13 | $value: Array.from(typedArray), 14 | }; 15 | }, 16 | 17 | checkSerialized(serialized) { 18 | return serialized.$type === klass.name; 19 | }, 20 | 21 | deserialize(serialized) { 22 | return new klass(serialized.$value); 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/types/never.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "never", 3 | serializedDescription: "never", 4 | 5 | check(val) { 6 | return false; 7 | }, 8 | 9 | serialize(bool) { 10 | throw new Error("Cannot serialize never"); 11 | }, 12 | 13 | checkSerialized(serialized) { 14 | return false; 15 | }, 16 | 17 | deserialize(serialized) { 18 | throw new Error("Cannot deserialize never"); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/types/null.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "null", 3 | serializedDescription: '{ $type: "null", $value: null }', 4 | 5 | check(val) { 6 | return val === null; 7 | }, 8 | 9 | serialize() { 10 | return { 11 | $type: "null", 12 | $value: null, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "null"; 18 | }, 19 | 20 | deserialize() { 21 | return null; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/number.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "number", 3 | serializedDescription: '{ $type: "number", $value: number }', 4 | 5 | check(val) { 6 | return ( 7 | typeof val === "number" && 8 | !Number.isNaN(val) && 9 | val !== Infinity && 10 | val !== -Infinity 11 | ); 12 | }, 13 | 14 | serialize(number) { 15 | return { 16 | $type: "number", 17 | $value: number, 18 | }; 19 | }, 20 | 21 | checkSerialized(serialized) { 22 | return serialized.$type === "number"; 23 | }, 24 | 25 | deserialize(serialized) { 26 | return serialized.$value; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/types/string.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "string", 3 | serializedDescription: '{ $type: "string", $value: string }', 4 | 5 | check(val) { 6 | return typeof val === "string"; 7 | }, 8 | 9 | serialize(str) { 10 | return { 11 | $type: "string", 12 | $value: str, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "string"; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/true.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "true", 3 | serializedDescription: '{ $type: "boolean", $value: true }', 4 | 5 | check(val) { 6 | return val === true; 7 | }, 8 | 9 | serialize(bool) { 10 | return { 11 | $type: "boolean", 12 | $value: bool, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "boolean" && serialized.$value === true; 18 | }, 19 | 20 | deserialize(serialized) { 21 | return serialized.$value; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/undefined.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: "undefined", 3 | serializedDescription: '{ $type: "undefined", $value: undefined }', 4 | 5 | check(val) { 6 | return val === undefined; 7 | }, 8 | 9 | serialize() { 10 | return { 11 | $type: "undefined", 12 | $value: undefined, 13 | }; 14 | }, 15 | 16 | checkSerialized(serialized) { 17 | return serialized.$type === "undefined"; 18 | }, 19 | 20 | deserialize() { 21 | return undefined; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | // "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | 3 | module.exports = { 4 | entry: "./src/client-test.js", 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /(node_modules)/, 10 | use: { 11 | loader: "babel-loader", 12 | }, 13 | }, 14 | ], 15 | }, 16 | plugins: [new HtmlWebpackPlugin()], 17 | }; 18 | --------------------------------------------------------------------------------