{"id":13605934,"url":"https://github.com/thoughtspile/banditypes","last_synced_at":"2025-05-16T07:07:25.867Z","repository":{"id":88736364,"uuid":"606856825","full_name":"thoughtspile/banditypes","owner":"thoughtspile","description":"🤠🧨 The mighty 400-byte schema validator for TS / JS","archived":false,"fork":false,"pushed_at":"2025-01-02T13:53:05.000Z","size":155,"stargazers_count":175,"open_issues_count":2,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-08T19:22:15.602Z","etag":null,"topics":["javascript","runtime-validation","schema","typescript","validation"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoughtspile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-26T19:07:55.000Z","updated_at":"2025-03-31T13:24:00.000Z","dependencies_parsed_at":"2023-12-19T11:03:31.671Z","dependency_job_id":"4b378167-6340-4fbf-8fd8-66a45f21ba70","html_url":"https://github.com/thoughtspile/banditypes","commit_stats":{"total_commits":38,"total_committers":4,"mean_commits":9.5,"dds":0.2894736842105263,"last_synced_commit":"6600a1cb30f99772d86d2d4c7cfed3499a394f8c"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditypes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditypes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditypes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditypes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoughtspile","download_url":"https://codeload.github.com/thoughtspile/banditypes/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254485065,"owners_count":22078767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","runtime-validation","schema","typescript","validation"],"created_at":"2024-08-01T19:01:04.434Z","updated_at":"2025-05-16T07:07:20.799Z","avatar_url":"https://github.com/thoughtspile.png","language":"TypeScript","funding_links":[],"categories":["typescript","Validation"],"sub_categories":["Reactive Programming"],"readme":"# Banditypes — the mighty 400-byte validator\n\nCheck if data conforms to a TS type at runtime — much like [zod,](https://zod.dev/) [yup](https://github.com/jquense/yup) or [superstruct](https://docs.superstructjs.org/), but in a tiny 400-byte package. Despite the small size, it's not a toy:\n\n- Passes the relevant superstruct test suite.\n- Rich built-in types: [maps, sets, tuples, literals,](#types) and generic [union types.](#or)\n- Mostly API-compatible with the established libraries.\n- Supports _both_ deriving TS types from schema _and_ [declaring a schema for an existing TS type.](#ts-first-schemas)\n- User-defined [types,](#cast-functions) [refinements, and conversions.](#map)\n- Decent performance — among the top libraries not using code generation.\n\nBanditypes is a 400-byte lib, tradeoffs have been made:\n\n- _No_ detailed errors with messages and paths, just a throw in a predictable location.\n- _No_ built-in refinements (empty, integer, etc.).\n- Compiled to ES2017: uses ...spreads and arrows. Can be transpiled further down.\n- Validation and conversion are mangled, so you have to use the returned object. \"Pure validation\" is impossible.\n- Some syntax might be a bit odd.\n\n\u003e Small size is the primary focus of banditypes. It's the smallest validation library, AFAIK, and I'll do my best to keep the core under 400 bytes (unless some critical bugs need fixing, in which case it might go slightly above that).\n\nThis is not a library for everybody, but it gets the job done, and it's small. Here's a usage example:\n\n```ts\nimport {\n  assert,\n  object,\n  number,\n  string,\n  array,\n  optional,\n  fail,\n  Infer,\n} from \"banditypes\";\n\nconst parseGunslinger = object({\n  name: string(),\n  kills: number(),\n  guns: array(string()),\n  born: object({\n    state: string().or(optional()),\n    year: number().map((n) =\u003e (Number.isInteger(n) ? n : fail())),\n  }),\n});\n\n// Explicit inference\ntype Gunslinger = Infer\u003ctypeof parseGunslinger\u003e;\n\nconst raw = JSON.parse(`{\n  \"name\": \"Dirty Bobby\",\n  \"kills\": 17,\n  \"guns\": [\"Colt 45\"],\n  \"born\": {\n    \"state\": \"Idaho\",\n    \"year\": 1872\n  }\n}`);\ntry {\n  const data = parseGunslinger(raw);\n  // fully type-safe access\n  console.log(`${data.name} from ${data.born.state} is out to kill ya`);\n} catch (err) {\n  console.log(\"invalid JSON\");\n}\n```\n\n400 bytes is an _approximate_ gzip bundle increase from using _all_ built-in validations. It may vary based on the minifier and the amount of validations used. A typical usage (primitives + object + array) is closer to 200 bytes, the core is around 100. Find out more about the [measurement technique.](#size-measurement)\n\nIf you like banditypes, check out [banditstash](https://github.com/thoughtspile/banditstash) — a tiny localStorage wrapper with runtime validation, fully configurable using plugins.\n\n## Table of contents\n\n- [Install](#install)\n- [Types](#types)\n- [Operators](#operators)\n  - [or](#or)\n  - [map](#map)\n- [Cast functions](#cast-functions)\n- [TS-first schemas](#ts-first-schemas)\n- [Size measurement](#size-measurement)\n- [Acknowledgements](#acknowledgements)\n- [License (it's MIT)](#license)\n\n## Install\n\n```sh\nnpm install --save banditypes\n```\n\n## Types\n\nbanditypes includes all the types you'd expect in a validation library:\n\n```ts\n// primitives\nstring();\nnumber();\nboolean();\n\n// always fails\nnever();\n// always passes\nunknown();\n\n// instanceof check\ninstance(MyClass);\n\n// checks if value is a function\n// static input / output validation is not possible in JS\nfunc();\n\n// { key: string; nullable: string | null; maybe?: string }\nobject({\n  key: string(),\n  // nullable field\n  nullable: string().or(nullable()),\n  // optional field\n  maybe: string().or(optional()),\n});\n// { key: string }, but don't remove other properties\nobjectLoose({\n  key: string(),\n});\n// number[]\narray(number());\n// Record\u003cstring, boolean\u003e\nrecord(boolean());\n\n// Set\u003cnumber\u003e\nset(number());\n// Map\u003cnumber, boolean\u003e\nmap(number(), boolean());\n// [number, string]\n// NOTE: \"as const\" must be used\ntuple([number(), string()] as const);\n\n// value comes from a set\nenums([1, 2]); // infers 1 | 2\n// mixed-type enums are OK:\nenums([true, 0, \"\"]);\n// literal type is a single-value enum:\nenums([42]);\n```\n\nEvery validator is just a function that returns the argument if it passes validation _or_ throws:\n\n```js\nconst yes = string()(\"ok\");\nconst no = string()(0);\n```\n\n- Non-primitive validators always clone the data passed.\n- `object` strips the keys not defined in the schema — to pass-through undeclared keys, use `objectLoose`.\n- `tuple` trims the undeclared tail of the array.\n- Object keys where validation returns `undefined` are stripped.\n- Strict object and tuple validations (that throw on undeclared keys) are not built-in.\n\n## Operators\n\nAs a luxury treat, every banditype has two methods: `map` for conversion and refinement, and `or` for making union types. I could strip around 17 bytes by turning these into functions, but I think it would make the library much less pleasant to use.\n\n### or\n\n`type1.or(type2)` passes input through `type2` _if_ `type1` fails. Useful for union types...\n\n```ts\nconst schema = string().or(number());\nschema(0); // ok\nschema(\"hello\"); // ok\nschema(null); // throws\ntype S = Infer\u003ctypeof schema\u003e; // string | number\n```\n\n...nullable or optional types...\n\n```ts\n// string | undefined\nconst optionalString = string().or(optional());\n// string | null\nconst optionalString = string().or(nullable());\n```\n\n...and default values — note that it is called on every validation error, not just missing values:\n\n```ts\nconst defaulted = string().or(() =\u003e \"Manos arriba\");\ndefaulted(\"hello\"); // 'hello'\ndefaulted(null); // 'Manos arriba'\ndefaulted({ hello: true }); // 'Manos arriba'\n```\n\n### map\n\n`banditype.map` can be used for type refinement: run the check and return the value if it passes, or `fail()`:\n\n```ts\nconst nonemptyString = string().map((s) =\u003e (s.length ? s : fail()));\nconst date = instance(Date).map((date) =\u003e\n  Number.isNaN(+date) ? fail() : date,\n);\n```\n\nOr to convert between types:\n\n```ts\nconst sum = array(number()).map((arr) =\u003e arr.reduce((acc, x) =\u003e acc + x, 0));\nsum([1, 2, 3]); // -\u003e 6\nsum([\"1\", \"2\", \"3\"]); // throws\nconst strFromNum = number().map(String);\nstrFromNum(9); // -\u003e '9'\nstrFromNum(\"9\"); // throws\n```\n\nOr _maybe_ as an intersection type, but the inferred type is always the type of the final cast, _not_ the intersection:\n\n```ts\nconst ab = objectLoose({ a: string() }).map(objectLoose({ b: string() }));\ntype AB = Infer\u003ctypeof ab\u003e; // { b: string }\n```\n\n## Cast functions\n\nCast functions are the central concept of banditypes: they accept `unknown` argument and return a value of type `T` or throw. These all are string-cast functions:\n\n```ts\nconst isString = (raw: unknown) =\u003e (typeof raw === \"string\" ? raw : fail());\nconst isNonemptyString = (raw: unknown) =\u003e\n  typeof raw === \"string\" \u0026\u0026 raw.length \u003e 0 ? raw : fail();\n```\n\nBut so are these, doing type conversion:\n\n```ts\nconst toString = (raw: unknown) =\u003e String(raw);\nconst toJson = (raw: unknown) =\u003e JSON.stringify(raw);\n```\n\nBare cast functions are allowed as arguments in collection types:\n\n```ts\nconst tag = Symbol();\nobject({\n  // unique symbol check\n  tag: (x) =\u003e (x === tag ? x : fail()),\n});\n// array of falsy values\narray((raw) =\u003e (!raw ? raw : fail()));\n```\n\nWrapping a cast in `banditype()` appends `.map` and `.or` methods, giving you a custom chainable type (note that the function you pass is mutated):\n\n```ts\nconst mySheriff = banditype\u003cMySheriff\u003e((raw) =\u003e\n  MySheriff.isSheriff(raw) ? raw : fail(),\n);\nconst angrySheriff = mySheriff.map((s) =\u003e (s.isAngry ? s : fail()));\n```\n\n## TS-first schemas\n\nUnlike _some_ validation libraries, banditypes support pre-defined TS schemas:\n\n```ts\ninterface Bank {\n  name: string;\n  money: number;\n}\nconst bankSchema = object\u003cBank\u003e({\n  name: string(),\n  money: number(),\n});\n```\n\nVery handy if your types are code-generated from GraphQL.\n\n## Size measurement\n\nThe 400-byte size reported assumes 5-pass terser and gzip. Brotli is slightly smaller, `esbuild` minification is slightly larger, but overall, banditypes is a very very small library. I don't think you can go much smaller. If you have any ideas on how to decrease the size further (_without_ throwing away the chainable API) — let me know!\n\nI use an unconventional (but sensible) approach to size measurement. Instead of measuring the gzip size of the library bundle, I build two versions of a \"sample app\" — one without validation, one using banditypes. This avoids measuring stuff that won't actually affect the bundle size:\n\n- `export` keywords and names — lib module is usually inlined, and export names are mangled.\n- 22-byte gzip End of Central Directory Record that's present in every gzipped file, so your app already has it.\n- repetitions of common JS syntax like `=\u003e` or `const`\n\nHowever, it also measures the code for integrating the library into user app — schema definition and actual validation. I can't do party tricks, removing functionality from library core, and making the user implement it manually. Otherwise, you could say \"I made a 0-byte library, but you have to check all the types yourself\". We optimize the overall bundle size when using the lib, not the lib size itself.\n\nThis technique can measure bundle size for different subsets of functionality (all validations; only primitives and objects; only core), and with different minifiers. This makes optimizing for tree-shaking and dead code elimination simple.\n\nThis is a great approach, especially for smaller libraries. Check out the samples and code in [`/bench`](./bench/)\n\n## Acknowledgements\n\n[Superstruct](https://github.com/ianstormtaylor/superstruct) was a major influence on banditypes with its modular design; shout out to [Ian Storm Taylor](https://twitter.com/ianstormtaylor) and all the contributors. I also borrowed superstruct's test suite.\n\n## License\n\n[MIT License](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtspile%2Fbanditypes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoughtspile%2Fbanditypes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtspile%2Fbanditypes/lists"}