Code Monkey home page Code Monkey logo

runtypes's Introduction

Runtypes Build Status Coverage Status

Safely bring untyped data into the fold

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.

Installation

npm install --save runtypes

Example

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 Runtypes 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.

Error information

When it fails to validate, your runtype emits a ValidationError object that contains detailed information that describes what's the problem. Following properties are available in the object:

  • name: Always "ValidationError"
  • message: A string that summarizes the problem overall
  • code: A Failcode that categorizes the problem
  • details: An object that describes which property was invalid precisely; only for complex runtypes (e.g. Record, Array, and the like)

If you want to inform your users about the validation error, it's strongly discouraged to rely on the format of message property in your code, as it may change across minor versions for readability thoughts. Instead of parsing message, you should use code and/or details property to programmatically inspect the validation error, and handle other stuff such as i18n.

Static type inference

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
}

Type guards

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 = []
		}
	}
}

Pattern matching

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. You should use it along with when helper function to enable type inference of the parameters of the case functions:

const makeANumber = match(
	when(Number, n => n * 3),
	when(Boolean, b => (b ? 1 : 0)),
	when(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(
	when(Number, n => n * 3),
	when(Boolean, b => (b ? 1 : 0)),
	when(String, s => s.length),
	when(Unknown, () => 42),
)

Constraint checking

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`

Template literals

The Template runtype validates that a value is a string that conforms to the template.

You can use the familiar syntax to create a Template runtype:

const T = Template`foo${Literal("bar")}baz`

But then the type inference won't work:

type T = Static<typeof T> // inferred as string

Because TS doesn't provide the exact string literal type information (["foo", "baz"] in this case) to the underlying function. See the issue microsoft/TypeScript#33304, especially this comment microsoft/TypeScript#33304 (comment) we hope to be implemented.

If you want the type inference rather than the tagged syntax, you have to manually write a function call:

const T = Template(["foo", "baz"] as const, Literal("bar"))
type T = Static<typeof T> // inferred as "foobarbaz"

As a convenient solution for this, it also supports another style of passing arguments:

const T = Template("foo", Literal("bar"), "baz")
type T = Static<typeof T> // inferred as "foobarbaz"

You can pass various things to the Template constructor, as long as they are assignable to string | number | bigint | boolean | null | undefined and the corresponding Runtypes:

// Equivalent runtypes
Template(Literal("42"))
Template(42)
Template(Template("42"))
Template(4, "2")
Template(Literal(4), "2")
Template(String.withConstraint(s => s === "42"))
Template(
	Intersect(
		Number.withConstraint(n => n === 42),
		String.withConstraint(s => s.length === 2),
		// `Number`s in `Template` accept alternative representations like `"0x2A"`,
		// thus we have to constraint the length of string, to accept only `"42"`
	),
)

Trivial items such as bare literals, Literals, and single-element Unions and Intersects are all coerced into strings at the creation time of the runtype. Additionally, Unions of such runtypes are converted into RegExp patterns like (?:foo|bar|...), so we can assume Union of Literals is a fully supported runtype in Template.

Caveats

A Template internally constructs a RegExp to parse strings. This can lead to a problem if it contains multiple non-literal runtypes:

const UpperCaseString = Constraint(String, s => s === s.toUpperCase(), {
	name: "UpperCaseString",
})
const LowerCaseString = Constraint(String, s => s === s.toLowerCase(), {
	name: "LowerCaseString",
})
Template(UpperCaseString, LowerCaseString)

The only thing we can do for parsing such strings correctly is brute-forcing every single possible combination until it fulfills all the constraints, which must be hardly done. Actually Template treats String runtypes as the simplest RegExp pattern .* and the “greedy” strategy is always used, that is, the above runtype won't work expectedly because the entire pattern is just ^(.*)(.*)$ and the first .* always wins. You have to avoid using Constraint this way, and instead manually parse it using a single Constraint which covers the entire string.

instanceof wrapper

If you have access to the class that you want to test values with the instanceof operator, then the InstanceOf runtype is exactly what you're looking for. Usage is straightforward:

class ObjectId { ... };
const ObjectIdChecker = InstanceOf(ObjectId);
ObjectIdChecker.check(value);

Function contracts

Runtypes along with constraint checking are a natural fit for enforcing function contracts. You can construct a contract from Runtypes 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

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("[email protected]")
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("[email protected]", "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>

Optional values

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()

Readonly records and arrays

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>

Helper functions for 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"'.
})

Related libraries

  • generate-runtypes Generates runtypes from structured data. Useful for code generators
  • json-to-runtypes Generates runtypes by parsing example JSON data
  • rest.ts Allows building type safe and runtime-checked APIs
  • runtypes-generate Generates random data by Runtype for property-based testing
  • runtyping Generate runtypes from static types & JSON schema
  • schemart Generate runtypes from your database schema.

runtypes's People

Contributors

72636c avatar artalar avatar bbenezech avatar breathe avatar chris-pardy avatar codemariner avatar davidje13 avatar dependabot[bot] avatar elderapo avatar flawyte avatar forbeslindesay avatar googol avatar greenkeeper[bot] avatar henrinormak avatar hzrdirl avatar iyegoroff avatar justingrant avatar keithlayne avatar kestred avatar liamolucko avatar mariusschulz avatar mitchellstern avatar pabloszx avatar pabra avatar patrickocoffeyo avatar pelotom avatar richgieg avatar runeh avatar simenandre avatar yuhr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

runtypes's Issues

Fails to build with declaration `true`

The following simple example fails to build for me when setting declaration to true in TypeScript's compilerOptions.

Code:

import * as rt from 'runtypes';

export const Test = rt.Record({ });

tsconfig.json:

{
  "include": [
    "src/conversion/test.ts"
  ],
  "compilerOptions": {
    "noEmit": false,
    "noUnusedLocals": true,
    "strict": true,
    "rootDir": ".",
    "outDir": "./lib",
    "declaration": true
  }
}

Error:

src/conversion/test.ts(3,14): error TS4023: Exported variable 'Test' has or is using name 'Record' from external module ".../node_modules/runtypes/lib/types/record" but cannot be named.

Any idea why that is?

Possible to make a RunType that uses instanceof?

Sometimes I'm expecting an instance of a particular class. Is there a way to make a constraint field based on instanceof check or other typeguard?

I want to express something like this ...

class PopulationDistribution { ... }

const Planet = Record({
  type: Literal('planet'),
  location: Vector,
  mass: Number,
  population: InstanceOf(PopulationDistribution),
  habitable: Boolean
})

Add a TaggedUnion runtype.

There's a fairly common pattern for using union types called tagged unions. Using the current Union runtype for these is functionally complete, however when a .check() fails, it's usually near impossible to work out why. I'd like to propose a new TaggedUnion runtype. You could use it as follows:

const AB = TaggedUnion("tag", {
  a: Record({ aprop: String }),
  b: Record({ bprop: Number }),
});

AB.check({ tag: "a", aprop: "foo" }); // success.
AB.check({ tag: "b", bprop: 10 }); // success.
AB.check({ tag: "c", cprop: "bar" }); // Error: Unknown tag: Expected "a" | "b", but was "c"
AB.check({ tag: "a", bprop: 10 }); // Error: Expected { tag: "a"; aprop: string; }, but was object.

AB.checkTag({ tag: "a" }); // true
AB.check({ tag: "a" }); // Error: Expected { tag: "a"; aprop: string; }, but was object.
AB.checkTag({ tag: "c", cprop: "bar" }); // false

Having a seperate checkTag is often useful for deserialising input from changing upstream providers such as webhooks where they may add new event types which you want to discard or log differently because you typically care less about them, but you do care if you get an event you thought you knew about which has a structure other than you were expecting.

As an example of why I feel this is necessary, the current approach leaves me with errors like the following while trying to deserialise Mailgun webhooks (if there is an error):

Error: Expected (((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { envelope: { sender?: string; sending-ip?: string; targets?: string; transport?: string; }; flags: { is-authenticated?: boolean; is-routed?: boolean; is-system-test?: boolean; is-test-mode?: boolean; }; message: { attachments: {}[]; size: number; }; }) & { delivery-status: { attempt-no: number; code: number; description: string; message: string; session-seconds: number; } & { certificate-verified?: boolean; mx-host?: string; retry-seconds?: number; tls?: boolean; utf8?: boolean; }; storage: { key: string; url: string; }; }) & { event: "delivered"; }) | ((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?:string; }; } & { recipient-domain?: string; }) & { client-info: { client-name: string; client-os: string; client-type: string; device-type: string; user-agent: string; }; geolocation: { city: string; country: string; region: string; }; ip: string; }) & { event: "opened"; }) | ((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { client-info: { client-name: string; client-os: string; client-type: string; device-type: string; user-agent: string; }; geolocation: { city: string; country: string; region: string; }; ip: string; }) & { event: "clicked"; url: string; }) | ((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { envelope: { sender?: string; sending-ip?: string; targets?: string; transport?: string; }; flags: { is-authenticated?: boolean; is-routed?: boolean; is-system-test?: boolean; is-test-mode?: boolean; }; message: { attachments: {}[]; size: number; }; }) & { event: "complained"; }) | ((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { client-info: { client-name: string; client-os: string; client-type: string; device-type: string; user-agent: string; }; geolocation: { city: string; country: string; region: string; }; ip: string; }) & { event: "unsubscribed"; }) | (((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { envelope: { sender?: string; sending-ip?: string; targets?: string; transport?: string; }; flags: { is-authenticated?: boolean; is-routed?: boolean; is-system-test?: boolean; is-test-mode?: boolean; }; message: { attachments: {}[]; size: number; }; }) & { delivery-status: { attempt-no: number; code: number; description: string; message: string; session-seconds: number; } & { certificate-verified?: boolean; mx-host?: string; retry-seconds?: number; tls?: boolean; utf8?: boolean; }; storage: { key: string; url: string; }; }) & { event: "failed"; reason: string; severity: "temporary"; }) | (((({ campaigns: always[]; id: string; log-level: string; message: { headers: { message-id: string; } & { from?: string; subject?: string; to?: string; }; }; recipient: string; tags: string[]; timestamp: number; user-variables: { cortex?: string; }; } & { recipient-domain?: string; }) & { envelope: { sender?: string; sending-ip?: string; targets?: string; transport?: string; }; flags: { is-authenticated?: boolean; is-routed?: boolean; is-system-test?: boolean; is-test-mode?: boolean; }; message: { attachments: {}[]; size: number; }; }) & { delivery-status: { attempt-no: number; code: number; description: string; message: string; session-seconds: number; } & { certificate-verified?: boolean; mx-host?: string; retry-seconds?: number; tls?: boolean; utf8?: boolean; }; storage: { key: string; url: string; }; }) & { event: "failed"; reason: string; severity: "permanent"; }), but was object

cannot use Static in *.d.ts file

I'd really like to be able to define my types in runtypes, and then use the type MyType = Static<typeof MyType> feature, but I cannot figure out how to incorporate this into my project

The examples show this all being done in one file, but what if I have a types.d.ts file in my project where I am declaring the types that are shared throughout multiple files? I tried importing the runtypes that I declared and the Static function into my types.d.ts file and then declaring the type there, but that seemed to make my d.ts file invalid.

Any ideas?

Thanks in advance and thanks for this project.

[Support] How to deal with enums and types defined by external libraries?

I have the following TypeScript code

import * as admin from "firebase-admin";

export enum Parser {
  STANDARD = "standard",
  FIVE_FILTERS = "fiveFilters",
}

export enum Status {
  DRAFT = "draft",
  PUBLISHED = "published",
}

export interface IWriter {
  feedName: string;
  writerRef: admin.firestore.DocumentReference;
}

export interface IEntryDocument {
  cronExpression: string;
  forceTeams: boolean;
  forceWriters: boolean;
  id: string;
  name: string;
  parser: Parser;
  status: Status;
  url: string;
  writers: IWriter[];
}

I need to validate the data that comes from the DB so I need to perform validation based on the IEntryDocument interface typings.

I started on converting these interfaces into runtypes format but I'm stuck with enums and external libraries' types.

This is where I reached so far:

import * as admin from "firebase-admin";
import {Boolean, String, Static, Array, Record} from "runtypes";

export enum Parser {
  STANDARD = "standard",
  FIVE_FILTERS = "fiveFilters",
}

export enum Status {
  DRAFT = "draft",
  PUBLISHED = "published",
}

const Writer = Record({
  feedName: String,
  // writerRef: admin.firestore.DocumentReference,
});

export interface IWriter {
  feedName: string;
  writerRef: admin.firestore.DocumentReference;
}

const EntryDocument = Record({
  cronExpression: String,
  forceTeams: Boolean,
  forceWriters: Boolean,
  id: String,
  name: String,
  // parser: Parser,
  // status: Status,
  url: String,
  writers: Array(Writer),
});

export interface IWriter extends Static<typeof Writer> {}
export interface IEntryDocument extends Static<typeof EntryDocument> {}

Have a look at the lines that are commented out. I'm stuck with 2 issues:

  1. I don't know what to do with admin.firestore.DocumentReference
  2. I don't know how to deal with Parser and Status enums

support for sanitization

Not sure whether this in scope of the library, but for example io-ts, joi, yup all etc. can sanitize values (i.e. coerce them within certain boundaries) which is actually useful in some use cases. It would be nice if runtypes could do that too. joi and yup all do sanitization for some of their built in types and in io-ts one can do sanitization in the validator function of custom types by passing the sanitized value to t.success(sanitizedValue).

I was actually looking at all the above mentioned libraries incl. runtypes for the use case of API validation. While I really wanted to use io-ts or runtypes (I use typescript everywhere) but I decided against the use of runtypes because of the lack of sanitization and it wasn't clear to me how to create custom types, and against io-ts because of the lack of an ability to create validation specific error messages.

Support for optional properties

Suggestion : support for optional properties on Record

  • .check would succeed if the optional properties are not present on the input object
  • the inferred static type would have the optional properties denoted by the ? sign.

Use case : OpenAPI schemas and Typescript support optional properties, thus such support in runtypes would help in handling request bodies on Web api calls. Same for JSON data coming from no-SQL databases, where all fields are not always present.

Map runtypes to JSON schema

Hi, Just reaching out to get some thoughts on potentially generating JSONschema from runtypes type representations, and if such a library exists to achieve this.

Just for some background, we currently have a project where we have several dozen json endpoints and for each endpoint, we have a TRequest and TResponse TypeScript type as well as a corresponding JSONSchema type (for both request, response). As you can imagine, this is quite the duplication, and we feel this duplication could potentially be removed with the use of runtypes.

One of our considerations however is we do like the idea of publishing TRequest / TResponse schema information to our clients (similar to WSDL). In fact our preference is to publish JSONSchema as a standard JSON schematic (analogous to XSD, WSDL) but to do so, we would need to map runtypes to JSONSchema somehow.

So wondering if there is functionality to do something similar to the following....

const Asteroid = Record({
  type: Literal('asteroid'),
  location: Vector,
  mass: Number,
});
// runtypes representation
// Asteroid = { tag: 'record',
//  fields:
//   { type:
//      { tag: 'literal',
//        value: 'asteroid',
//        check: [Function],

const AsteroidSchema = Asteroid.toJsonSchema()
// json schema representation
// AsteroidSchema = {
//     type: 'object',
//     properties: {
//       type: {type: 'string'},
//       location: {
//          x: { type: 'number'},
//          y: { type: 'number'},
//       ...

From this, we would be able to integrate runtypes with our existing json schema validators (we are currently using the ajv json schema validator configured for draft 6).

Any information you can provide would be great.

Kind Regards
Sinclair

`check()` returns any for nested types

In this simple example:

const SubValue = Record({
    a: Number,
});

const ParentValue = Record({
    v: Array(SubValue),
});

const pv = ParentValue.check({});

The type returns from check() is {v: any[]}. But when I look at the type of ParentValue it's fully realized. What am I missing?

rt.Static<> can't extract static type for Runtype.Record with nested Record ...?

I'm noticing that I can't seem to correctly extract the static type from Runtype's with nested rt.Record({}).

const Example = rt.Record({
  b: rt.Record({
    c: rt.String
  })
});

type StaticExample = rt.Static<typeof Example>;

Effective type of StaticExample (as reported by vscode):

type StaticExample = {
    b: any;
}

And this produces no type error ...

const x: StaticExample = {
  b: ""
};

Am I doing something wrong or is this a bug or a known limitation? I'm using runtypes 2.0.3 with typescript 2.7.2 and this tsconfig:

{
  "compileOnSave": true,
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./lib",
    "lib": ["es2017", "es2017.object", "esnext.asynciterable"],
    "strict": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es2015",
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "inlineSourceMap": true,
    "inlineSources": true,
    "forceConsistentCasingInFileNames": true,
    "strictFunctionTypes": false,
    "noErrorTruncation": true,
    "strictPropertyInitialization": false
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules"]
}

Possible for union.match to support methods which return different type's ...?

It might be convenient if the match(...) method on union types were able to support functions which return different types ...

Maybe it be possible to implement .match() in such a way as to support a signature like this ...?

export interface Match2<A extends Rt, B extends Rt> {
    <Z1, Z2>(a: Case<A, Z1>, b: Case<B, Z2>): Matcher2<A, B, Z1, Z2>;

Instead of:

export interface Match2<A extends Rt, B extends Rt> {
    <Z>(a: Case<A, Z>, b: Case<B, Z>): Matcher2<A, B, Z>;

I took a stab at it with below PR ... Kind of ugly but seems to work? Is this a better type for .match() maybe? Without it the constraints on the functions provided to .match(...) are very tight ...

Straw-man example of what doesn't work without this change ...:

const Type1 = rt.Record({ status: rt.Literal('failure'), msg: rt.String });
const Type2 = rt.Record({
  status: rt.Literal('success'),
  username: rt.String,
  authenticationState: rt.String,
});
const Response = rt.Union(Type1, Type2);

const extracted = Response.match(
  failure => {
    return { status: failure.status, authenticationState: 'not_logged_in', data: { ...failure } };
  },
  success => {
    return {
      status: success.status,
      authenticationState: success.authenticationState,
      data: { ...success },
    };
  },
);

No way to skip optional field check

Hi,

I tried the following:

const OptionalString = String.Or(Undefined);
type OptionalString = Static<typeof OptionalString>;


const IOrganization = Record({
    id: NonEmptyString,
    name: NonEmptyString,
    description: OptionalString,
    link: OptionalString,
});
type IOrganization = Static<typeof IOrganization>; 

if I call

const o: IOrganization = {
      id: "a",
      name: "the name",
};
IOrganization.check(o);

The check function throws an error! There is a way to avoid the throwing if an optional field is missing in the argument object?

Coerce isn't the right term

Very minor nitpick, but coerce means forcing something to happen, or to change. Does runtime's coerce method do anything apart from checking that an object is already conforming to the type?

(Otherwise, very interesting library!)

[0.7.0] -> [0.8.0] Dictionary regression

Considering

const Errors = RT.Dictionary(RT.Array(RT.String))
type ErrorsType = RT.Static<typeof Errors>

0.7.0 correct

type ErrorsType = {
    [_: string]: string[];
}

0.8.0 incorrect

type ErrorsType = {
    [_: string]: Arr<String>;
}

Do you know intuitively what is causing this or would you like me to investigate further on?

Not urgent, I can stick on 0.7.0 for a while, it works perfectly for me.

Thanks again.

`[]` passes `Record` `check`

I have a type like the following specified:

const ConfigType = rt.Record({})
.And(rt.Partial({
  x: rt.String,
}));

Unfortunately running the following code succeeds without error:

ConfigType.check([])

Support for nested/recursive types?

Hi there,

this library looks amazing and seems like what i am currently missing in native typescript.
But there is one thing, i hadn't success to achieve with this library: nested recursive types.
I need to define the structure types for an object literal like:

type item = {
  Prop1: string,
  Prop2: number
}

type group = {
  Prop3: string,
  Items: Array<item | group>
}

const JSON: group = {
  Prop3: "Root",
  Items: [
    {
      Prop1: "Item 1",
      Prop2: 123
    },
    {
      Prop3: "Nested Group 1",
      Items: [
        /*
          ... and so on
        */
      ]
    }
  ]
}

If i try to achieve this with runtypes the following way:

const item = Record({
  Prop1: String,
  Prop2: Number
});

const group = Record({
  Prop3: String,
  Items: Array(Union(item, group))
});

it gives a compiler error "block-scoped variable 'group' is used before its declaration".
If i replace const group with var group the compiler error disappears but typescript recognizes group only as type any..

Is there any way to describe the desired structure?
Thanks for any hint on this...

Regression on the "More descriptive errors" feature

It seems that #51 has regressed in later versions.

In 2.0.5 I get error messages like

Expected restEndpoint to be string

but in 2.1.6 they are just

Expected string, but was undefined

Reproduce with

const Options = rt.Record({
    restEndpoint: rt.String,
    userId: rt.String,
});
Options.check({userId: ""});

Is there any way to declare generic key?

TypeScript

interface Foo {
  [s: string]: number;
}

Is there any way to declare a runtype for the same? The following does't work. I assume there is a way that I don't know.

const Foo = Record({
  [s: string]: Number,
})

Add a flag to check/validate/guard which allows enforcing exact match (not just structurally assignable)

Perhaps I'm missing something fundamental here about how this library makes assertions, but the following obviously errors in Typescript:

interface ISeries {
  name: string;
  value: number;
}

const series: ISeries = {
  name: 'test',
  value: 10,
  foo: true,
};

but this check() does not error with runtypes:

const Series = Record({
  name: String,
  value: Number,
});

Series.check({
  name: 'test',
  value: 10,
  foo: true
});

Why is the additional key foo allowed through in this case?

Install error in PowerShell (VS Code)

Within the integrated PowerShell of Visual Studio Code i cannot install Runtypes.
It fails with the error:

PS C:\Source\Ionic\flexApp> npm install runtypes --save
npm ERR! path C:\Source\Ionic\flexApp\node_modules\.staging\runtypes-20e0969c
npm ERR! code EPERM
npm ERR! errno -4048
npm ERR! syscall rename
npm ERR! Error: EPERM: operation not permitted, rename 'C:\Source\Ionic\flexApp\node_modules\.staging\runtypes-20e0969c' -> 'C:\Source\Ionic\flexApp\node_modules\runtypes'
npm ERR!     at Error (native)
npm ERR!  { Error: EPERM: operation not permitted, rename 'C:\Source\Ionic\flexApp\node_modules\.staging\runtypes-20e0969c' -> 'C:\Source\Ionic\flexApp\node_modules\runtypes'
npm ERR!     at Error (native)
npm ERR!   cause:
npm ERR!    { Error: EPERM: operation not permitted, rename 'C:\Source\Ionic\flexApp\node_modules\.staging\runtypes-20e0969c' -> 'C:\Source\Ionic\flexApp\node_modules\runtypes'
npm ERR!        at Error (native)
npm ERR!      errno: -4048,
npm ERR!      code: 'EPERM',
npm ERR!      syscall: 'rename',
npm ERR!      path: 'C:\\Source\\Ionic\\flexApp\\node_modules\\.staging\\runtypes-20e0969c',
npm ERR!      dest: 'C:\\Source\\Ionic\\flexApp\\node_modules\\runtypes' },
npm ERR!   stack: 'Error: EPERM: operation not permitted, rename \'C:\\Source\\Ionic\\flexApp\\node_modules\\.staging\\runtypes-20e0969c\' -> \'C:\\Source\\Ionic\\flexApp\\node_modules\\r
untypes\'\n    at Error (native)',
npm ERR!   errno: -4048,
npm ERR!   code: 'EPERM',
npm ERR!   syscall: 'rename',
npm ERR!   path: 'C:\\Source\\Ionic\\flexApp\\node_modules\\.staging\\runtypes-20e0969c',
npm ERR!   dest: 'C:\\Source\\Ionic\\flexApp\\node_modules\\runtypes',
npm ERR!   parent: 'flexapp' }
npm ERR!
npm ERR! Please try running this command again as root/Administrator.

Whereas the same command (without Admin privileges!) is running successfully in the normal Windows commandline:

C:\Source\Ionic\flexApp>npm install runtypes --save
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: ts-jest@19.0.6 (node_modules\runtypes\node_modules\ts-jest):
npm WARN enoent SKIPPING OPTIONAL DEPENDENCY: ENOENT: no such file or directory, rename 'C:\Source\Ionic\flexApp\node_modules\runtypes\node_modules\ts-jest' -> 'C:\Source\Ionic\flexApp\node_modules\runtypes\node_modules\.ts-jest.DELETE'

+ runtypes@0.10.1
added 427 packages in 83.487s

Any Idea whats going wrong in Powershell within VS Code?

Is converting values in the scope of this library ?

A common use case when validating data from external APIs is to have dates encoded as strings.
Let's say I get the following data from a JSON API:

{ "date": "2015-10-12 12:00:00" }

I would want to represent it with the following Runtype:

Record({ date: InstanceOf(Date) })

Is there a simple way to go from the first to the second without having to use an intermediate Record({ date: String }) runtype, and doing the validation ?

I think that having a convertFrom method on the Runtype interface would help a lot.

interface Runtype<A> {
  // ...
  convertFrom<B> : (checkBefore: (x: any) => B) => Runtype<A>;
}

The returned Runtype would be exactly the same, but the check method would be changed to include the additional validation in checkBefore before the one from the initial Runtype.

I could then do:

const Input = Record({ 
  date: InstanceOf(Date).convertFrom(x => {
    const strX = String.check(x);
    const timestamp = Date.parse(strX);
    if(isNan(timestamp)) {
      throw new ValidationError('Invalid date string ' + strX);
    }
    return new Date(timestamp);
 })
});

Do you think it could be added to the library (I would be willing to do it)?

Option to create tagged Union from Union() runtype ...?

It would be pretty nice if runtypes supported the ability to make a tagged Union from a runtype Union by adding an _type field not present in the original data with appropriate value ... It would be useful when validating weird/legacy json structures that you don't control ...

Something like

const Asteroid = Record({
  location: Vector,
  mass: Number,
})

const Planet = Record({
  location: Vector,
  mass: Number,
  population: Number,
  habitable: Boolean,
})

const SpaceObject = TaggedUnion(Asteroid, Planet)

const asteroid = { ...}
const planet = { ... }

const asteroidOrPlanet = SpaceObject.check(asteroid)
console.log(asteroidOrPlanet._type) // "Asteroid"
console.log(SpaceObject.check(planet))._type // "Planet"

Automatic argument checking

Sometimes we need type-check multiples of multiple parameters which are passed from the external world i.e. there're some callbacks to implement, and appending type-checking code for each param to every one of their function bodies is kind of painful. In such case I write a decorator which reduces keypresses more or less and makes functions' logic innate-looking, for example,

@checked(Runtype1, Runtype2, Runtype3)
method(param1: Static1, param2: Static2, param3: Static3) {
  ...
}

instead of

method(param1: Static1, param2: Static2, param3: Static3) {
  Runtype1.check(param1)
  Runtype2.check(param2)
  Runtype3.check(param3)
  ...
}

How do you think about adding this feature to your library?

Incorrect generic type constraint for interface Record?

Hi,
When I open record.ts in Visual Studio Code with TypeScript 2.8.1, the following error is shown:

image

Current definition:

export interface Record<O extends { [_ in never]: Runtype }>
  extends Runtype<{ [K in keyof O]: Static<O[K]> }> {
  tag: 'record';
  fields: O;
}

Isn't the following more appropriate? If not, I would appreciate if you explain me why! I don't understand the current constraint for the generic type.

export interface Record<O extends { [_: string]: Runtype }> // [_: string] instead of [_ in never]
  extends Runtype<{ [K in keyof O]: Static<O[K]> }> {
  tag: 'record';
  fields: O;
}

Thank you in advance.

How to type a Date object?

First off thanks for a really useful library. I've already used it very successfully on a small project.

However, now I've hit a problem with typing a Date object. I'm probably missing something fundamental here.

With the following:

import { Number, Record, InstanceOf, Static } from 'runtypes'

const Coords = Record({
  latitude: Number,
  longitude: Number,
  time: InstanceOf(Date)
})

type Coords = Static<typeof Coords>

let data: Coords = {
  latitude: 1.0,
  longitude: 1.0,
  time: new Date(2017, 0, 1, 10, 0, 0)
}

I get the type error:

...
Types of property 'time' are incompatible.
        Type 'Date' is not assignable to type 'DateConstructor'.

What am I missing?

if this is not the correct way to type a Date object, what is?

Array index

Hi Tom,

How would you model {[k: string]: any}, if possible ?

I tried

RT.Record({
    [RT.String as any as string]: RT.String
})

It typechecks correctly to RT.RunType<[x: string]: string;>, but runtime checks does not work as expected.

`check` should be able to return branded type

The check method of a runtype with constraint returns a value of the type without constraint, so sometimes I need to use branded types like this:

const A = String.withConstraint(x => x === 'a');
type A = Static<typeof A> & { __brandA: any };
const x: string = 'a';
const a: A = A.check(x); // Type 'string' is not assignable to type 'A'.
const a: A = A.check(x) as A; // OK but looks verbose

Then maybe it's good that check or some another (defined anew?) method can return some user defined type, for example:

let A = String.withConstraint(x => x === 'a');
type A = Static<typeof A> & { __brandA: any };
A = Brand<A>(A);
const x: string = 'a';
const a: A = A.verify(x); // Should be OK

I think this is still ugly but absolutely better than doing as A whenever A.check(x).

How do you think?

How does this compare to io-ts ?

Hi! Kudos for sharing this library. I'm doing some validation on incoming data from http API, and this made it so much easier, compared to implementing types, and then also validators.

I also found that another project io-ts seem to do very similar thing. I wonder if there are any fundamental difference to the taken approach?

Creating objects

I have the following code:

import { Boolean, Number, String, Literal, Array, Tuple, Record, Union } from 'runtypes';

const Asteroid = Record({
  type: Literal('asteroid'),
  location: Vector,
  mass: Number,
});

Is there any easy way to create an object of type Asteroid? Ideally, I'd like to use something like:

Asteroid.create({type: 'asteroid', location: [], mass: 1}); //valid
Asteroid.create({}); // error - the args supplied don't meet the contraints

Is this possible? I noticed that none of the examples is creating objects at all.

Checking objects on values only?

Is there any runtypes equivalent of following type definition? I don't want to specify key strings but check if the type of their values is string.

type Json = { [key: string]: string };

How to represent 'any' types?

I am trying to convert an existing codebase to use runtypes.

However I am struggling to work out how to represent the arguments key here with a typescript any type:

export type Description = {
  name: string;
  type: 'fanout' | 'topic' | 'direct';
  durable?: boolean;
  internal?: boolean;
  autoDelete?: boolean;
  alternateExchange?: string;
  arguments?: any;
};

So far I have this:

Record({
  name: String,
  type: Union(Literal('fanout'), Literal('topic'), Literal('direct'))
}).And(
  Partial({
    alternateExchange: String,
    autoDelete: Boolean,
    durable: Boolean,
    internal: Boolean
    //arguments: Any // Any is not exported from runtypes
  })
)

Runtime type checking works:

Description.check({
  name:'foo',
  type: 'fanout',
  arguments: {
    ding:'pop'
  }
}); // no errors

However the following causes type errors in editor:

const myDescription: Static<typeof Description> ={
  name:'foo',
  type: 'fanout',
  arguments: { // shows error 
    ding:'pop'
  }
}

The error is

Object literal may only specify known properties, and 'arguments' does not exist in type...

How do I infer this property should be any?

Serialization/Deserialization of Runtypes

Love this library. We're thinking about using it to type and validate our api responses. One problem we've encountered though is that for very large response objects validation isn't fast enough (can take up to a few seconds in some cases). A possible solution we've come up with is using web workers to perform this validation since it isn't essential we know right away if the response json is invalid. Problem is getting the runtype into the web worker. If we could serialize/deserialize the runtype we'd be able to pass it to the worker. It seems like it'd be pretty trivial to serialize/deserialize a runtype provided it's representing json. Thoughts on this? Would serialization of runtypes be a feature you'd be willing to include in this library?

Return multiple errors

Would it be possible for validate/check to return all errors instead of just the first?

This would be useful for validating user input, giving the user all errors instead of just one.

Provide error meta data

It would be great to provide error meta data, something like this:

let error = {
 success: false,
 message: '...',
 path: ['key1', 'key2', ...],
}

This can be useful for form validation because we validate the whole form with an object type, but need to show the message in each field.

Make error messages at least as informative as those given by TypeScript

Some examples of TypeScript's static error messages:

;(): number | boolean => 'hello'
// [ts] Type '"hello"' is not assignable to type 'number | boolean'.

;(): { x: number } => ({})
// [ts]
// Type '{}' is not assignable to type '{ x: number; }'.
//   Property 'x' is missing in type '{}'.

;(): { x: number } => ({ x: 'hello' })
// [ts]
// Type '{ x: string; }' is not assignable to type '{ x: number; }'.
//   Types of property 'x' are incompatible.
//     Type 'string' is not assignable to type 'number'.
// Record({ x: Number }).check({ x: 'hello' })

;(): { x: number } | { x: boolean } => ({ x: 'hello' })
// [ts]
// Type '{ x: "hello"; }' is not assignable to type '{ x: number; } | { x: boolean; }'.
//   Type '{ x: "hello"; }' is not assignable to type '{ x: boolean; }'.
//     Types of property 'x' are incompatible.
//       Type '"hello"' is not assignable to type 'boolean'.

;(): { x: number } | { x: boolean } => ({})
// [ts]
// Type '{}' is not assignable to type '{ x: number; } | { x: boolean; }'.
//   Type '{}' is not assignable to type '{ x: boolean; }'.
//     Property 'x' is missing in type '{}'.

Some notes:

  • One thing we will need to do this fully is a way to infer the type of a given value (or at least a string representation of it), although this is may be more of a nice-to-have.
  • We already have string representations of runtypes, via show
  • Most type errors are flat, but records and unions can have hierarchical errors. In both cases it would be an improvement over TypeScript's static error messages if we allowed multiple sub-errors:
    • For records, multiple sub-errors represent all the fields which are missing or of the wrong type (I believe doing this correctly would address #8)
    • For unions, multiple sub-errors represent the ways in which the supplied value failed to conform to each alternative in turn. TypeScript union error messages are confusing because they show you only how it failed to match the last alternative, which may well be different than the type you were attempting to match.

Support for optional properties

Currently the Record type's validation verifies that a property exists before checking their types. This causes problems when some properties on a Record should be optional.

Current Behavior:

Record({
 id: String,
 name: Union(Undefined,String)
}).validate({id: '1234'}).success === false

Expected Behavior:

Record({
 id: String,
 name: Union(Undefined,String)
}).validate({id: '1234'}).success === true

Also acceptable:

Record({
 id: String,
 name: Optional(String)
}).validate({id: '1234'}).success === true

Current Workaround:

Intersect(
  Record({
    id: String,
  }),
 Partial({
   name: String
 })
).validate({id: '1234'}).success === true

Partialise a Record type?

const Info = Record({
  x: Number
}).And(Partial({
  y: Number
}));
const AllPartialInfo = Partialize(Info);
type AllPartialInfo = Static<typeof AllPartialInfo>;
// -> { x?: number | undefined; } & { y?: number | undefined; }

Is there something like above Partialize?

Extracting union of strings to runtime array?

I am curious if that's possible somehow? You see, I am using library mobx-state-tree which is also checked at runtime, but currently, it's impossible to use Typescript types for modeling.

type State = 'Unknown' | 'Online' | 'Offline'

const Model = types.model({
  connState: types.enumeration(['Unknown', 'Online', 'Offline'])
})

The first step should be probably to support Symbol.iterator and then I can do this.

const State = Union(
  Literal('Unknown'),
  Literal('Online'),
  Literal('Offline')
)

const Model = types.model({
  connState: types.enumeration(Array.from(State))
})

However, that's not enough since it would give an array of Literal objects. I am not how to proceed there to keep the ease of use. Perhaps some custom method like State.toArray() could be more feasible?

Related library: rest.ts

Hi @pelotom ,

I'm developing a module to bridge the safety gap across HTTP APIs, with first-class support for runtypes.

The module is called rest.ts and it lets you write API definitions which you can bind to client and servers in order to benefit from type safety on both ends of the API.
When used with runtypes, the server-side module validates all incoming data automatically.

I thought you might like this module because basically it is to HTTP APIs what runtypes is to basic types. It's almost like a natural extension of your work.

Please give it a try if you can and let me know what you think of it, and if you think it's worthy of the "related libraries" section of the readme.

"Better" support for nullable properties in Record?

Hi,

at first i didn't know, how to declare nullable properties in a Record and if it is possible at all.
The only way i found so far is:

const A = Record({
  T: String,
  N: Number
}).And(Partial({
  Z: Boolean
}));

which results in a type:

type AType = {
    T: string;
    N: number;
} & {
    Z?: boolean;
}

Wouldn't it be more easy and intuitive if one could do:

const A = Record({
  T: String,
  N: Number,
  Z: Nullable(Boolean) // or Boolean.AsNullable()
  /*
       or even better as this is the normal typescript syntax?:
       Z?: Boolean
  */
});

Something like that shouldn't be that hard, or do i miss something?

Thanks and regards..

Static type not recognizing that a field is nullable

Hi, first of all. Thanks for this awesome package! I've noticed that typescript is not recognizing that a Record field is .Or(Null). Instead typescript is inferring it to be non-null.

Is this expected behaviour?

See the screenshot below:

screen shot 2019-02-17 at 10 19 48 am

Add example of creating an interface from Static to README

When reading the README, as a TypeScript n00b, I thought “oh, this is so close—if only I could use Static<T> to make an interface because that would be more usable for me than type.” I didn’t realize I could just use:

interface IAsteroid extends Static<typeof Asteroid> {}

I think it could be helpful to add an example of this to the README.

Suggestions around pattern matching

First of all, thank you for putting such an amazing library. It utilizes both typescript and javascript really well, producing such an empowering and comprehensive developer experience :)

I found your project while looking at the ways to pattern match in typescript. So my suggestions are biased.

Let's say we have this:

const One = Record({ tag: Literal('one'), descOfOne: String });
const Two = Record({ tag: Literal('two'), descOfTwo: String });

type OneOrTwo = Static<typeof One> | Static<typeof Two>;

While matching I want to extract payload as well. Something like:

const DescriptionOfOne = One.pipe(x=>x.descOfOne); 
//not Runtype anymore, can be used only for pattern matching

// OneOrTwo | null -> string 
const getDescription = matchCase<OneOrTwo | null>()
  .of(DescriptionOfOne, desc => desc) // acceptes extracted prop
  .of(Two, ({descOfTwo}) => descOfTwo) // normal types can be used just fine
  .of(Void, ()=> 'nothing');

DescriptionOfOne has different type now. Lets say Pattern<One, string>.

const NonEmptyDesc = DescriptionOfOne.withContraint( desc => desc.lenght >0) 
// Error! Will not compile. Patterns can only be mapped over.

const OneDescLenght = DescriptionOfOne.map(decs => desc.lenght);
// totally valid though.

I think it makes sense to map them further to produce different pipelines.

But if we use Records extensively for matching we have perf issues :( "check" checks all props recursively.

const TaggedOne = Tag(One, 'tag'); //new Runtype that remembers which one is the tag
// disables deep tree matching and checks only tags. 
// useful for application types (like redux actions)
// It might make sense to introduce Tagged({shape}) and TaggedLiteral('one') for that purpose as well.

Also with typescript 2.8 we will have a proper Diff<T,U>. And if I understand it correctly it opens this

const EmptyOne = One.withConstraint( one => one.descOfOne.length === 0);

//Possible from typescript 2.8 with a help of Diff
const getDescription = matchCase<OneOrTwo | null>()
  .of(EmptyOne, _ => 'empty one') // leaves the same type
  .guard(One, _ => "one") // removes One, so after we can guard only "Two | Void"
  .guard(Two, _ => "two") // leaves just "Void" to check
  .guard(Void, ()=> 'nothing'); // "never" after this point
  .exhaustive() // can be applied only to "never". 
// So it should give compile time error if not all types are ruled out
// .default(()=>'def val') same as of(Always, ()=>{}).

Note probably constrained runtypes cannot be guarded. Otherwise PositiveNumber can rule out Number < 0. Note: guarding on non constrained Patterns should be totally fine.

++ Side note:
Looking through the sources I noticed that "validate" uses try/catch from "check" to produce boolean result. Was there a specific reason for that? I guess if we hit an if statement that "validates" to false often there might be significant overhead for try/catch as well as allocating a Result object.
Does it make sense to have validateInternal(): true | string, aka either passed or failed with message. And then "check" can throw with a proper message.

+++ Side note n2:
You library is brilliant and inspiring. I felt that I love my job even more just thinking about these suggestions :)

I would love to collaborate/contribute if you like these ideas

Am-m-mazing!

Using this package for 3 months already in production projects, who would've thought it can be such a game changer, thank you!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.