Type Safe Object Notation & Validation
npm install @skarab/tsonType Safe Object Notation & Validation
  !GitHub code size in bytes !GitHub  
š Work in Progress, not ready for production...
- š§± Functional
- š· Immutable
- ā
Well tested
After a contribution to the tRPC project, I wanted to understand more deeply the use of generics and inference in TypeScript.
I needed a challenge so I set myself the goal of coding my own schema validation library.
This library is heavily inspired by Zod (_I try to provide the same API_) but in order to avoid cloning it, I challenged myself to not use any classes.
``bash`
pnpm add @skarab/tson
_yarn and npm also works_
`ts`
import { t } from "tson";
`ts`
const { t } = require("tson");
`ts
import { t } from "tson";
const name = t.string();
name.parse("nyan"); // return "nyan"
name.parse(42); // throw TypeCheckError
`
`ts
import { t } from "tson";
const user = t.object({
name: t.string(),
age: t.number(),
admin: t.boolean(),
});
user.parse({ name: "nyan", age: 42, admin: true });
type User = t.infer
// { name: string, age: number, admin: boolean }
`
It is strongly recommended to activate the strict mode of TypeScript which will activate all checking behaviours that results in stronger guarantees of the program's correctness.
By default tson parse objects in STRICT mode, this means that all undefined values in a scheme will be considered as an error. You can change this behaviour globally or locally, the procedure is documented here.
- tson
- Features
- Why?
- Install
- ES and CommonJS module
- Examples
- Strict mode
- TypeScript
- tson
- Table of contents
- API
- First level types
- Primitive types
- Numbers types
- Empty types
- Catch-all types
- Never type
- literal(value)
- array(type)
- tuple(...type)
- [tuple(type[])](#tupletype-1)
- [tuple(type[] as const)](#tupletype-as-const)
- object(schema)
- object(schema, mode)
- object helpers
- .strict()
- .strip()
- .passthrough()
- union(...type)
- [union(type[])](#uniontype-1)
- [union(type[] as const)](#uniontype-as-const)
- optional(type)
- enum(...string)
- Access enum properties
- Access enum values
- Test enum values
- Infer enum type
- [enum(string[])](#enumstring-1)
- [enum(string[] as const)](#enumstring-as-const)
- enum(object)
- enum(object as const)
- enum(enum)
- nativeEnum(enum)
- instanceof(type)
- date()
- record(type)
- set(type)
- set(...type)
- [set([type, ...type])](#settype-type)
- map(keyType, valueType)
- map(schema)
- promise(type)
- function()
- function(args)
- function(args, returns)
- function(args, returns, implement)
- preprocess(filter, type)
- postprocess(filter, type)
- postprocess(filter, inputType, outputType)
- Type helpers
- safeParse(input)
- optional()
- preprocess()
- postprocess()
- Contributing š
`ts`
t.string();
t.number();
t.bigint();
t.boolean();
t.symbol();
t.date();
`ts`
t.nan();
t.finite();
t.infinity();
t.integer(); // Alias: int()
t.unsignedNumber(); // Alias: unumber()
t.unsignedInteger(); // Alias: uinteger(), uint()
`ts`
t.undefined();
t.null();
t.void();
`ts`
t.any();
t.unknown();
`ts`
t.never();
`ts
const life = t.literal(42);
const love = t.literal(true);
const name = t.literal("nyan");
life.value; // type => 42
`
`ts`
const arr1 = t.array(t.string()); // string[]
const arr2 = t.array(t.boolean()); // boolean[]
`ts`
const tpl = t.tuple(t.string(), t.number(), t.string()); // [string, number, string]
`ts`
const tpl = t.tuple([t.string(), t.number(), t.string()]); // [string, number, string]
š The following code does not work, TypeScript can not infer array values properly. Use the as const workaround to do this.
`ts`
const types = [t.string(), t.number(), t.string()];
const tpl = t.tuple(types); // [string, number, string]
`ts`
const types = [t.string(), t.number(), t.string()] as const;
const tpl = t.tuple(types); // [string, number, string]
`ts
const user = t.object({
name: t.string(),
age: t.number(),
admin: t.boolean(),
});
type User = t.infer
// { name: string, age: number, admin: boolean }
`
By default tson parse objects in STRICT mode, but you can change the mode globally or locally.
There are three modes:
- STRICT: Will raise an error if a key is not defined in the schema.STRIP
- : Strips undefined keys from the result and does not raise an error.PASSTHROUGH
- : Keeps undefined keys and does not raise an error.
Change the default mode globally.
`ts`
t.defaultSettings.objectTypeMode = t.ObjectTypeMode.STRIP;
Change the mode locally.
`ts
const schema = { a: t.string(), b: t.string() };
const input = { a: "a", b: "b", c: "c" };
const user = t.object(schema, t.ObjectTypeMode.STRICT);
user.parse(input); // throws an TypeParseError
const user = t.object(schema, t.ObjectTypeMode.STRIP);
user.parse(input); // { a: string, b: string }
const user = t.object(schema, t.ObjectTypeMode.PASSTHROUGH);
user.parse(input); // { a: string, b: string, c: string }
`
`ts`
t.object(schema).strict();
// same as
t.object(schema, t.ObjectTypeMode.STRICT);
`ts`
t.object(schema).strip();
// same as
t.object(schema, t.ObjectTypeMode.STRIP);
`ts`
t.object(schema).passthrough();
// same as
t.object(schema, t.ObjectTypeMode.PASSTHROUGH);
`ts`
const uni = t.union(t.string(), t.number()); // string | number
`ts`
const tpl = t.union([t.string(), t.number(), t.string()]); // string | number
š The following code does not work, TypeScript can not infer array values properly. Use the as const workaround to do this.
`ts`
const types = [t.string(), t.number(), t.string()];
const tpl = t.union(types); // string | number
`ts`
const types = [t.string(), t.number(), t.string()] as const;
const tpl = t.union(types); // string | number
`ts`
const user = t.object({
name: t.string(),
age: t.optional(t.number()),
});
// { name: string, age?: number }
`ts`
const myEnum = t.enum("UP", "DOWN", "LEFT", "RIGHT");
`ts
myEnum.enum.UP; // === "UP"
myEnum.enum.PLOP; // error: PLOP does not exists
myEnum.enum.DOWN = "prout"; // error: it is read-only
(property) enum: {
readonly UP: "UP";
readonly DOWN: "DOWN";
readonly LEFT: "LEFT";
readonly RIGHT: "RIGHT";
}
`
`ts
myEnum.options[1]; // === "DOWN"
(property) options: ["UP", "DOWN", "LEFT", "RIGHT"]
`
`ts`
myEnum.parse(myEnum.enum.LEFT); // => "LEFT"
myEnum.parse("LEFT"); // => "LEFT"
myEnum.parse("2"); // => "LEFT"
myEnum.parse(2); // => "LEFT"
myEnum.parse("PLOP"); // error: expected '0|1|2|3|UP|DOWN|LEFT|RIGHT' got 'string'
`ts
type MyEnum = t.infer
function move(direction: MyEnum) {
// direction === "DOWN"
}
move(myEnum.enum.DOWN);
`
`ts`
const myEnum = t.enum(["UP", "DOWN", "LEFT", "RIGHT"]);
š The following code does not work, TypeScript can not infer array values properly. Use the as const workaround to do this.
`ts`
const values = ["UP", "DOWN", "LEFT", "RIGHT"];
const myEnum = t.enum(values);
`ts`
const myEnum = t.enum(["UP", "DOWN", "LEFT", "RIGHT"] as const);
`ts`
const values = ["UP", "DOWN", "LEFT", "RIGHT"] as const;
const myEnum = t.enum(values);
`ts`
const myEnum = t.enum({ UP: "UP", DOWN: "DOWN", LEFT: 42, RIGHT: 43 });
š The following code does not work, TypeScript can not infer object properties properly. Use the as const workaround to do this.
`ts`
const values = { UP: "UP", DOWN: "DOWN", LEFT: 42, RIGHT: 43 };
const myEnum = t.enum(values);
`ts`
const values = { UP: "UP", DOWN: "DOWN", LEFT: 42, RIGHT: 43 } as const;
const myEnum = t.enum(values);
`ts
enum MyEnum {
UP = "UP",
DOWN = "DOWN",
LEFT = 42,
RIGHT,
}
const myEnum = t.enum(MyEnum);
`
Alias: enum(enum)
`ts
enum MyEnum {
UP = "UP",
DOWN = "DOWN",
LEFT = 42,
RIGHT,
}
const myEnum = t.nativeEnum(MyEnum);
`
`ts
class MyClass {}
const instance = new MyClass();
t.instanceof(MyClass).parse(instance); // passes
t.instanceof(MyClass).parse("nyan"); // fail
`
`ts`
t.date().parse(new Date()); // passes
t.date().parse("2022-01-12T00:00:00.000Z"); // passes
t.date().parse("not a string date"); // fail
`ts`
t.record(t.string()); // { [x: string]: string }
t.record(t.number()); // { [x: string]: number }
t.record(t.date()); // { [x: string]: Date }
Testing a single type on the entire set
`ts`
t.set(t.string()); // Set
Testing a union of types on the entire set
`ts`
t.set(t.union(t.string(), t.boolean(), t.string())); // Set
Same as tuple(...type) but test if the input is an instance of Set.
Testing a tuple of types on the Set
`ts`
t.set(t.string(), t.boolean(), t.string()); // Set<[string, boolean, string]>
t.set([t.string(), t.boolean(), t.string()]); // Set<[string, boolean, string]>
`ts`
t.map(t.string(), t.number()); // Map
t.map(t.date(), t.string()); // Map
Same as object(schema) but test if the input is an instance of Map.
`ts
const map = new Map();
t.map({ name: t.string(), size: t.string() }).parse(map);
`
`ts
const promise = t.promise(t.number());
await promise.parse(Promise.resolve(42)); // resolve: 42
await promise.parse(Promise.resolve("42")); // reject: expected 'number' got 'string'
await promise.parse(42); // reject: expected 'Promise' got 'number'
`
`ts
const func = t.function();
type Func = t.infer
`
`ts
const func = t.function([t.string(), t.number()]);
type Func = t.infer
`
`ts
const func = t.function([t.string()], t.boolean());
type Func = t.infer
`
`ts
const args = [t.string(), t.boolean()] as const;
const returns = t.union(t.string(), t.number());
const func = t.function(args, returns, (input, toInt) => {
// input type is string and toInt type is boolean
return toInt ? parseInt(input) : input.toUpperCase();
});
type Func = t.infer
`
If you want to modify the input before it is parsed you can use the preprocess type as follows.
`ts
const toString = t.preprocess((input) => String(input), t.string());
toString.parse("42"); // => "42"
toString.parse(42); // => "42"
`
If you want to modify the output after it is parsed you can use the postprocess type as follows.
`ts
const postprocess = t.postprocess((input) => input + 2, t.number());
postprocess.parse(40); // => 42
postprocess.parse("42"); // throws: "expected 'number' got 'string'"
`
If you want to modify the output after it is parsed you can use the postprocess type as follows.
`ts
const postprocess = t.postprocess(
(input) => String(input),
t.number(),
t.string(),
);
postprocess.parse(40); // => "42"
postprocess.parse("42"); // => throws: "expected 'number' got 'string'"
`
If you want to avoid the parse method throws an error you can use the .safeParse() method instead.
`ts
t.bigint().safeParse(42n);
// => { success: true, data: 42n }
t.bigint().safeParse(42);
// => {
// "error": [TypeParseError: expected 'bigint|undefined' got 'number'],
// "success": false,
// }
`
`ts
t.bigint().optional(); // => bigint | undefined
// same as
t.optional(t.bigint());
`
`ts
t.string().preprocess((input) => String(input));
// same as
t.preprocess((input) => String(input), t.string());
`
Alias: .transform()
`ts
t.number().postprocess((input) => input + 2);
// same as
t.postprocess((input) => input + 2, t.number());
``
See CONTRIBUTING.md