ruti v0.0.17
WORK IN PROGRESS!
Ruti is still in development. I won't call it production ready, but the test coverage is close to 100% and I've been using it in production for months. Everything is subject to change, so pay close attention when updating (even between semver patches)!
Ruti
Ruti is a TypeScript library for type checking at runtime.
Introduction
TypeScript is missing one major feature: runtime type checking. This library is an attempt at solving this issue.
But to do type checking at runtime you first need to make your types accessible at runtime. A simple way to achieve this is to declare your types as JavaScript values inside your code instead of TypeScript types, and to then generate TypeScript types from those values. That way you can use the TypeScript types as usual, but you can also use the JavaScript values at runtime. This is the approach Ruti takes.
The JavaScript values, later referred to as Arg (placeholder name!), are human-readable and resemble TypeScript type declarations somewhat. Because they are optimized for human usability they are not used directly for type checking. Instead they are used to generate machine friendly data structures, referred to as Templates. These templates, together with the generated TypeScript types and the value you want to type check, can then be passed to Ruti's type checking function at any time in your code for runtime type checking!
I'm also experimenting with ways of safely merging two objects of the same type (merge_state).
Examples
You can find code examples in the "examples" directory.
Use npm run example <name> to run an example (replace <name> with help for more information).
Quickstart
The following code is also hosted on CodeSandbox, in case you prefer a more interactive experience.
Setting up a template
// Create a type declaration object
// (The somewhat weird way you declare types for Ruti)
const person_arg = {
name: 'string',
age: 'number',
address: {
street: 'string',
zip_code: 'number',
},
nicknames: [['string']],
} as const;
// Generate a template
// (Creates machine-friendly data from the human-readable data)
const person_template = create_template(person_arg);
// Generate a TypeScript type
type Person = FromTTypeArg<typeof person_arg>Creating and updating state
// Create initial state
const person: Person = {
name: 'Peter',
age: 30,
address: {
street: 'Somewhere 2',
zip_code: 12345,
},
nicknames: ['Pete', 'Big P'],
};
// Update state
// Note: This does not manipulate "person", instead it returns a copy
const updated_person = merge_state(person_template, person, { age: 31 });
// person.age === 30
// updated_person.age === 31Checking types
const person = {
name: 'Johan',
age: 55,
address: {
street: 'Somewhere 4',
zip_code: 12345,
},
nicknames: ['Mr J'],
};
if (is_type<Person>(person_template, person)) {
// Note: is_type uses a type guard, so "person" is of type "Person" inside this scope!
console.log(`"person" is a Person! Their name is ${person.name}.`);
} else {
console.log('"person" is not a Person. :(');
}Terms
There are three main concepts in Ruti: arg, template and state.
Note: Arg and State are not very fitting terms. They should be replaced.
Arg is the human-friendly type declaration that the TypeScript type and template are generated from.
Template is the machine-friendly translation of the type declaration. It is used at runtime to determine what types and shapes different states should have (to remain type safe).
State is the data that is type checked against a template at runtime. This includes both the data being modified and the data modifying it.
Functions
create_template(arg)
This generates a template from an arg. The purpose is to make the type declarations more concise and readable (and also to make it easier to generate TypeScript types from). This function is optional, you can write templates by hand or load them at runtime.
Arguments
arg: Arg to generate a template from.
Returns A template corresponding to arg. (TODO: Be more specific & list edge cases)
Throws if ...
argis not a valid Arg.
merge_state(template, a, b, opts?)
Merge b into a and return the result. No argument is modified by this function. Objects are merged recursively (and arrays are not).
Arguments
template: Template thataalready conforms to, andbis compared against.a: Current state.b: State to apply toa.opts: Options for how to merge the states. These options are applied to child objects recursively. (Optional)
Returns ...
a, ifaandbare strictly equal.b, ifbis a primitive value (boolean,number,string,nullorundefined).- A new array, if
bis an array. The new array is a copy ofb. - A new object, if
bis an object. The new object is a copy ofa(or an empty object ifais not an object) with all properties (listed intemplate.children) ofbapplied to it (if a property is an object, then this function is applied to that as well).
Throws if ...
bis not of a type listed intemplate.types.bis an array, and contains a value of a type not that is not listed intemplate.contents.bis an array, andtemplate.contentscontainsobject(because merging arrays of objects is not supported!).bis an object, and contains a property that is not listed intemplate.children(unlessopts.ignore_extraistrue).bis an object, and contains a property that is listed intemplate.children, but the types of the properties does not match.bis an object,ais not an object, andbdoes not contain every property listed intemplate.children.templateis not a valid template (this may not catch all template issues).
is_type(template, value, opts?, on_fail?)
Check if value conforms to template.
Arguments
template: Template to check conformity against.value: Value to check conformity with.opts: Options for how to perform the conformity check. These options are applied to child objects recursively. (Optional)on_fail: Called ifvaluedoes not conform totemplate. Arguments contains information about how it failed. (Optional)
Returns true if value comforms to template, otherwise false.
Throws if ...
templateis not a valid template (this may not catch all template issues).
Arg structure
Data types
There are 7 different data types, and they are divided into two categories:
Primitive: boolean, number, string, null and undefined.
Advanced: object and array.
Syntax
Each value can be one of the following:
Format: value => type
- A single primitive type
- Example:
'string'=>string
- Example:
- An array of primitive types (this results in a union of all the types)
- Example:
['string', 'number']=>string | number
- Example:
- An array inside another array (this results in an array)
- Example:
[['boolean']]=>boolean[] - Example 2:
[['string', 'number']]=>(string | number)[] - Example 3:
[['boolean'], ['string', 'number']]=>boolean[] | (string | number)[]
- Example:
- An object with primitive or advanced types
- Example:
{ x: 'number' }=>{ x: number } - Example:
{ y: [['string']] }=>{ y: string[] } - Example:
{ z: { w: 'string' } }=>{ z: { w: string } }
- Example:
Notes:
- Unions may not contain more than one object (including inside an array if it is part of the union).
[[ { x: 'number' } ], 'string']and[[ 'number' ], { y: 'string' }]are fine.[[ { x: 'number' } ], { y: 'string' }]is forbidden.
- Arrays only support primitive types and up to one object. No nested arrays!
[[ { x: 'number' } ]]and[[ { x: 'number' }, 'string' ]]are fine.[[ { x: 'number' }, { y: 'string' } ]]and[[ { x: 'number' }, ['string'] ]]are forbidden.
- Arrays with objects are NOT supported by
merge_state(because I haven't decided what way they should merge). - Ruti treats
nullas its own primitive (even though it's treated as anobjectby Javascript).