Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Runtypes is a TypeScript library that provides runtime validation and type checking for JavaScript objects. It allows developers to define types and validate data against those types at runtime, ensuring that the data conforms to the expected structure and types.
Basic Type Validation
Runtypes allows you to define basic types like String and Number and validate data against these types. If the data does not match the expected type, an error is thrown.
const { String, Number } = require('runtypes');
const Name = String;
const Age = Number;
Name.check('John Doe'); // Passes
Age.check(30); // Passes
// Name.check(123); // Throws an error
// Age.check('30'); // Throws an error
Object Validation
Runtypes allows you to define complex types like objects and validate data against these types. You can specify the structure of the object and the types of its properties.
const { Record, String, Number } = require('runtypes');
const Person = Record({
name: String,
age: Number
});
Person.check({ name: 'John Doe', age: 30 }); // Passes
// Person.check({ name: 'John Doe', age: '30' }); // Throws an error
Union Types
Runtypes supports union types, allowing you to define a type that can be one of several types. This is useful for validating data that can have multiple valid types.
const { Union, String, Number } = require('runtypes');
const StringOrNumber = Union(String, Number);
StringOrNumber.check('Hello'); // Passes
StringOrNumber.check(123); // Passes
// StringOrNumber.check(true); // Throws an error
Array Validation
Runtypes allows you to define array types and validate data against these types. You can specify the type of the elements in the array.
const { Array, String } = require('runtypes');
const StringArray = Array(String);
StringArray.check(['Hello', 'World']); // Passes
// StringArray.check(['Hello', 123]); // Throws an error
Optional Properties
Runtypes allows you to define optional properties in objects. This is useful for validating data where some properties may or may not be present.
const { Record, String, Number, Optional } = require('runtypes');
const Person = Record({
name: String,
age: Optional(Number)
});
Person.check({ name: 'John Doe' }); // Passes
Person.check({ name: 'John Doe', age: 30 }); // Passes
// Person.check({ name: 'John Doe', age: '30' }); // Throws an error
io-ts is a runtime type system for IO decoding/encoding in TypeScript. It provides similar functionality to runtypes, allowing you to define types and validate data at runtime. However, io-ts has a more functional programming approach and integrates well with fp-ts.
Zod is a TypeScript-first schema declaration and validation library. It provides a similar feature set to runtypes, allowing you to define schemas and validate data at runtime. Zod is known for its simplicity and ease of use.
Yup is a JavaScript schema builder for value parsing and validation. It is similar to runtypes in that it allows you to define schemas and validate data. Yup is widely used in the React ecosystem, especially with form libraries like Formik.
Runtypes allow you to take values about which you have no assurances and check that they conform to some type A
.
This is done by means of composable type validators of primitives, literals, arrays, tuples, records, unions,
intersections and more.
npm install --save runtypes
Suppose you have objects which represent asteroids, planets, ships and crew members. In TypeScript, you might write their types like so:
type Vector = [number, number, number];
type Asteroid = {
type: 'asteroid';
location: Vector;
mass: number;
};
type Planet = {
type: 'planet';
location: Vector;
mass: number;
population: number;
habitable: boolean;
};
type Rank = 'captain' | 'first mate' | 'officer' | 'ensign';
type CrewMember = {
name: string;
age: number;
rank: Rank;
home: Planet;
};
type Ship = {
type: 'ship';
location: Vector;
mass: number;
name: string;
crew: CrewMember[];
};
type SpaceObject = Asteroid | Planet | Ship;
If the objects which are supposed to have these shapes are loaded from some external source, perhaps a JSON file, we need to
validate that the objects conform to their specifications. We do so by building corresponding Runtype
s in a very straightforward
manner:
import { Boolean, Number, String, Literal, Array, Tuple, Record, Union } from 'runtypes';
const Vector = Tuple(Number, Number, Number);
const Asteroid = Record({
type: Literal('asteroid'),
location: Vector,
mass: Number,
});
const Planet = Record({
type: Literal('planet'),
location: Vector,
mass: Number,
population: Number,
habitable: Boolean,
});
const Rank = Union(
Literal('captain'),
Literal('first mate'),
Literal('officer'),
Literal('ensign'),
);
const CrewMember = Record({
name: String,
age: Number,
rank: Rank,
home: Planet,
});
const Ship = Record({
type: Literal('ship'),
location: Vector,
mass: Number,
name: String,
crew: Array(CrewMember),
});
const SpaceObject = Union(Asteroid, Planet, Ship);
(See the examples directory for an expanded version of this.)
Now if we are given a putative SpaceObject
we can validate it like so:
// spaceObject: SpaceObject
const spaceObject = SpaceObject.check(obj);
If the object doesn't conform to the type specification, check
will throw an exception.
In TypeScript, the inferred type of Asteroid
in the above example is
Runtype<{
type: 'asteroid'
location: [number, number, number]
mass: number
}>
That is, it's a Runtype<Asteroid>
, and you could annotate it as such. But we don't really have to define the
Asteroid
type in TypeScript at all now, because the inferred type is correct. Defining each of your types
twice, once at the type level and then again at the value level, is a pain and not very DRY.
Fortunately you can define a static Asteroid
type which is an alias to the Runtype
-derived type like so:
import { Static } from 'runtypes';
type Asteroid = Static<typeof Asteroid>;
which achieves the same result as
type Asteroid = {
type: 'asteroid';
location: [number, number, number];
mass: number;
};
In addition to providing a check
method, runtypes can be used as type guards:
function disembark(obj: {}) {
if (SpaceObject.guard(obj)) {
// obj: SpaceObject
if (obj.type === 'ship') {
// obj: Ship
obj.crew = [];
}
}
}
The Union
runtype offers the ability to do type-safe, exhaustive case analysis across its variants using the match
method:
const isHabitable = SpaceObject.match(
asteroid => false,
planet => planet.habitable,
ship => true,
);
if (isHabitable(spaceObject)) {
// ...
}
There's also a top-level match
function which allows testing an ad-hoc sequence of runtypes:
const makeANumber = match(
[Number, n => n * 3],
[Boolean, b => (b ? 1 : 0)],
[String, s => s.length],
);
makeANumber(9); // = 27
To allow the function to be applied to anything and then handle match failures, simply use an Unknown
case at the end:
const makeANumber = match(
[Number, n => n * 3],
[Boolean, b => (b ? 1 : 0)],
[String, s => s.length],
[Unknown, () => 42],
);
Beyond mere type checking, we can add arbitrary runtime constraints to a Runtype
:
const Positive = Number.withConstraint(n => n > 0);
Positive.check(-3); // Throws error: Failed constraint check
You can provide more descriptive error messages for failed constraints by returning
a string instead of false
:
const Positive = Number.withConstraint(n => n > 0 || `${n} is not positive`);
Positive.check(-3); // Throws error: -3 is not positive
You can set a custom name for your runtype, which will be used in default error
messages and reflection, by using the name
prop on the optional options
parameter:
const C = Number.withConstraint(n => n > 0, { name: 'PositiveNumber' });
To change the type, there are two ways to do it: passing a type guard function
to a new Runtype.withGuard()
method, or using the familiar
Runtype.withConstraint()
method. (Both methods also accept an options
parameter to optionally set the name.)
Using a type guard function is the easiest option to change the static type, because TS will infer the desired type from the return type of the guard function.
// use Buffer.isBuffer, which is typed as: isBuffer(obj: any): obj is Buffer;
const B = Unknown.withGuard(Buffer.isBuffer);
type T = Static<typeof B>; // T is Buffer
However, if you want to return a custom error message from your constraint
function, you can't do this with a type guard because these functions can only
return boolean values. Instead, you can roll your own constraint function and
use the withConstraint<T>()
method. Remember to specify the type parameter for
the Constraint
because it can't be inferred from your check function!
const check = (o: any) => Buffer.isBuffer(o) || 'Dude, not a Buffer!';
const B = Unknown.withConstraint<Buffer>(check);
type T = Static<typeof B>; // T will have type of `Buffer`
One important choice when changing Constraint
static types is choosing the
correct underlying type. The implementation of Constraint
will validate the
underlying type before running your constraint function. So it's important to
use a lowest-common-denominator type that will pass validation for all expected
inputs of your constraint function or type guard. If there's no obvious
lowest-common-denominator type, you can always use Unknown
as the underlying
type, as shown in the Buffer
examples above.
Speaking of base types, if you're using a type guard function and your base type
is Unknown
, then there's a convenience runtype Guard
available, which is a
shorthand for Unknown.withGuard
.
// use Buffer.isBuffer, which is typed as: isBuffer(obj: any): obj is Buffer;
const B = Guard(Buffer.isBuffer);
type T = Static<typeof B>; // T will have type of `Buffer`
Runtypes along with constraint checking are a natural fit for enforcing function
contracts. You can construct a contract from Runtype
s for the parameters and
return type of the function:
const divide = Contract(
// Parameters:
Number,
Number.withConstraint(n => n !== 0 || 'division by zero'),
// Return type:
Number,
).enforce((n, m) => n / m);
divide(10, 2); // 5
divide(10, 0); // Throws error: division by zero
Branded types is a way to emphasize the uniqueness of a type. This is useful until we have nominal types:
const Username = String.withBrand('Username');
const Password = String.withBrand('Password').withConstraint(
str => str.length >= 8 || 'Too short password',
);
const signIn = Contract(Username, Password, Unknown).enforce((username, password) => {
/*...*/
});
const username = Username.check('someone@example.com');
const password = Password.check('12345678');
// Static type OK, runtime OK
signIn(username, password);
// Static type ERROR, runtime OK
signIn(password, username);
// Static type ERROR, runtime OK
signIn('someone@example.com', '12345678');
Branded types are like opaque types and work as expected, except it is impossible to use as a key of an object type:
const StringBranded = String.withBrand('StringBranded');
type StringBranded = Static<typeof StringBranded>;
// Then the type `StringBranded` is computed as:
// string & { [RuntypeName]: "StringBranded" }
// TS1023: An index signature parameter type must be either `string` or `number`.
type SomeObject1 = { [K: StringBranded]: number };
// Both of these result in empty object type i.e. `{}`
type SomeObject2 = { [K in StringBranded]: number };
type SomeObject3 = Record<StringBranded, number>;
// You can do like this, but...
const key = StringBranded.check('key');
const SomeRecord = Record({ [key]: Number });
// This type results in { [x: string]: number }
type SomeRecord = Static<typeof SomeRecord>;
// So you have to use `Map` to achieve strongly-typed branded keys
type SomeMap = Map<StringBranded, number>;
Runtypes can be used to represent a variable that may be undefined.
// For variables that might be `string | undefined`
Union(String, Undefined);
String.Or(Undefined); // shorthand syntax for the above
Optional(String); // equivalent to the above two basically
String.optional(); // shorthand syntax for the above
The last syntax is not any shorter than writing Optional(String)
when you import Optional
directly from runtypes
, but if you use scoped import i.e. import * as rt from 'runtypes'
, it would look better to write rt.String.optional()
rather than rt.Optional(rt.String)
.
If a Record
may or may not have some properties, we can declare the optional properties using Record({ x: Optional(String) })
(or formerly Partial({ x: String })
). Optional properties validate successfully if they are absent or undefined
or the type specified.
// Using `Ship` from above
const RegisteredShip = Ship.And(
Record({
// All registered ships must have this flag
isRegistered: Literal(true),
// We may or may not know the ship's classification
shipClass: Optional(Union(Literal('military'), Literal('civilian'))),
// We may not know the ship's rank (so we allow it to be undefined via `Optional`),
// we may also know that a civilian ship doesn't have a rank (e.g. null)
rank: Optional(Rank.Or(Null)),
}),
);
There's a difference between Union(String, Undefined)
and Optional(String)
iff they are used within a Record
; the former means "it must be present, and must be string
or undefined
", while the latter means "it can be present or missing, but must be string
or undefined
if present".
Prior to v5.2, Union(..., Undefined)
in a Record
was passing even if the property was missing. Although some users considered this behavior was a bug especially for the sake of mirroring TS behavior, it was a long-standing thing, and some other users have been surprised with this fix. So the v5.2 release has been marked deprecated on npm, due to the breaking change.
Note that null
is a quite different thing than undefined
in JS and TS, so Optional
doesn't take care of it. If your Record
has properties which can be null
, then use the Null
runtype explicitly.
const MilitaryShip = Ship.And(
Record({
shipClass: Literal('military'),
// Must NOT be undefined, but can be null
lastDeployedTimestamp: Number.Or(Null),
}),
);
You can save an import by using nullable
shorthand instead. All three below are equivalent things.
Union(Number, Null);
Number.Or(Null);
Number.nullable();
Array and Record runtypes have a special function .asReadonly()
, that creates a new runtype where the values are readonly.
For example:
const Asteroid = Record({
type: Literal('asteroid'),
location: Vector,
mass: Number,
}).asReadonly();
type Asteroid = Static<typeof Asteroid>;
// { readonly type: 'asteroid', readonly location: Vector, readonly mass: number }
const AsteroidArray = Array(Asteroid).asReadonly();
type AsteroidArray = Static<typeof AsteroidArray>;
// ReadonlyArray<Asteroid>
Record
Record
runtype has the methods .pick()
and .omit()
, which will return a new Record
with or without specified fields (see Example section for detailed definition of Rank
and Planet
):
const CrewMember = Record({
name: String,
age: Number,
rank: Rank,
home: Planet,
});
const Visitor = CrewMember.pick('name', 'home');
type Visitor = Static<typeof Visitor>; // { name: string; home: Planet; }
const Background = CrewMember.omit('name');
type Background = Static<typeof Background>; // { age: number; rank: Rank; home: Planet; }
Also you can use .extend()
to get a new Record
with extended fields:
const PetMember = CrewMember.extend({
species: String,
});
type PetMember = Static<typeof PetMember>;
// { name: string; age: number; rank: Rank; home: Planet; species: string; }
It is capable of reporting compile-time errors if any field is not assignable to the base runtype. You can suppress this error by using @ts-ignore
directive or .omit()
before, and then you'll get an incompatible version from the base Record
.
const WrongMember = CrewMember.extend({
rank: Literal('wrong'),
// Type '"wrong"' is not assignable to type '"captain" | "first mate" | "officer" | "ensign"'.
});
Runtype
for property-based testingFAQs
Runtime validation for static types
We found that runtypes demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.