Skip to content

Experimental schema builder using TypeScript template strings.

License

Notifications You must be signed in to change notification settings

alexharri/strema

Repository files navigation

strema

Experimental schema builder using TypeScript template strings.

Concept

The inner workings of how this library works are explained in this blog post: Build your own schema language with TypeScript's infer keyword.

Usage

Use compileSchema to create a schema.

import { compileSchema } from "strema";

const schema = compileSchema(`{
  message: string;
  size: number <positive, int>;
  tags: string[];
  author: {
    name: string;
    email: string <email>;
    age: number <int, min(18)>;
  };
}`);

To get the type from the schema, use ExtractSchemaType.

import { ExtractSchemaType } from "strema";

type Body = ExtractSchemaType<typeof schema>;

To validate and parse incoming data, use the parseSync method on schema.

// Throws an error if req.body does not conform to the schema
const body = schema.parseSync(req.body);

Overview

Field types

This library supports four field types.

Primitives

There are three primitive types.

String

Schema TypeScript type
const schema = compileSchema(`{
  value: string;
}`);
{ value: string }
// Optional 'value' field
const schema = compileSchema(`{
  value?: string;
}`);
{ value: string | null }
// Optional 'value' field with default value
const schema = compileSchema(`{
  value?: string = "Hello, world";
}`);
{ value: string }
// Apply email rule to 'to' field
const schema = compileSchema(`{
  to: string <email>;
}`);
{ to: string }
String rules
  • min(n) sets a minimum length for the string.
  • max(n) sets a maximum length for the string.
  • length(n) equivalent to min(n), max(n).
  • email the string value must be an email address.
  • uuid the string value must be a uuid.

Number

Schema TypeScript type
const schema = compileSchema(`{
  value: number;
}`);
{ value: number }
// Optional 'value' field
const schema = compileSchema(`{
  value?: number;
}`);
{ value: number | null }
// Optional 'value' field with default value
const schema = compileSchema(`{
  value?: number = 1;
}`);
{ value: number }
// Apply min rule to 'value' field
const schema = compileSchema(`{
  value: number <min(1)>;
}`);
{ value: number }
Number rules
  • min(n) sets a minimum value for the number.
  • max(n) sets a maximum value for the number.
  • int the value must be an integer.
  • positive equivalent to min(0).

Boolean

Schema TypeScript type
const schema = compileSchema(`{
  include: boolean;
}`);
{ include: boolean }
// Optional 'include' field
const schema = compileSchema(`{
  include?: boolean;
}`);
{ include: boolean | null }
// Optional 'include' field with default value
const schema = compileSchema(`{
  include?: boolean = false;
}`);
{ include: boolean }

Booleans do not support any rules.

Objects

Objects fields represent a collection of sub-fields (key-value pairs).

Schema TypeScript type
const schema = compileSchema(`{
  book: {
    name: string;
    description: string;
    author: {
      name: string;
      age: number;
    };
  };
}`);
{
  book: {
    name: string;
    description: string;
    author: {
      name: string;
      age: number;
    };
  };
}

To create an object with dynamic keys, use a Record.

Arrays

Arrays represent a list of values. The list may be multidimensional.

Schema TypeScript type
const schema = compileSchema(`{
  tags: string[];
}`);
{ tags: string[] }
const schema = compileSchema(`{
  matrix: number[][];
}`);
{ matrix: number[][] }
const schema = compileSchema(`{
  items: { name: string }[];
}`);
{ items: Array<{ name: string }> }

You can create arrays of any type. Here's how you would represent an array of objects.

Schema TypeScript type
const schema = compileSchema(`{
  books: {
    name: string;
    description: string;
  }[];
}`);
{
  books: Array<{
    name: string;
    description: string;
  }>;
}

Records

Records represent a collection key-value pairs with dynamic keys (i.e. hash maps, dictionaries, associative arrays). The record syntax is Record<K, V> where K is the key type and V is the value type.

K must be either string or number. V can be any value that this library supports.

Schema TypeScript type
const schema = compileSchema(`{
  map: Record<string, number>;
}`);
{ map: Record<string, number> }
const schema = compileSchema(`{
  map: Record<string, {
    value: number;
  }>;
}`);
{ map: Record<string, { value: number }> }
const schema = compileSchema(`{
  map: Record<string, number[]>;
}`);
{ map: Record<string, number[]> }

Rules

Primitive types support rules to perform basic validation. Rules are specified inside of <> after the type name and before ; with multiple rules separated by ,. If the rule takes an argument, provide it inside of () after the rule name.

Schema TypeScript type
const schema = compileSchema(`{
  age: number <positive, int>;
  email: string <email>;
  password: string <min(8)>;
}`);
{
  age: number;
  email: string;
  password: string;
}

The available rules can be found here:

Rules may also be applied to arrays (and multidimensional arrays). In those cases, specify the rules after the [] array notation.

Schema TypeScript type
const schema = compileSchema(`{
  tags: string[] <min(1)>;
  coords: number[][] <int>;
}`);
{
  tags: string[];
  coords: number[][];
}

Rules can not be applied directly to arrays or objects.

Optional fields

By default, all fields are required. To mark a field as optional, add a ? after the field name:

Schema TypeScript type
const schema = compileSchema(`{
  description?: string;
}`);
{ description: string | null }

Optional primitives

When a primitive field is optional, null and undefined values are not rejected.

const schema = compileSchema(`{
  description?: string;
}`);

const output = schema.parseSync({ description: undefined });

console.log(output);
//=> { description: null }

Optional arrays

Optional arrays behave in the same way as primitives.

const schema = compileSchema(`{
  tags?: string[];
}`);

const output = schema.parseSync({ tags: undefined });

console.log(output);
//=> { tags: null }

Optional objects

Object fields behave the same as primitives and arrays, with the exception that object fields with no required fields accept null and undefined.

const schema = compileSchema(`{
  options: { notify?: boolean; delay?: number };
}`);

const output = schema.parseSync({ options: undefined });

console.log(output.options);
//=> { notify: null, delay: null }

However, if the object is optional, it resolves to null when null or undefined are provided.

const schema = compileSchema(`{
  options?: { notify?: boolean; delay?: number };
}`);

const output = schema.parseSync({ options: undefined });

console.log(output.options);
//=> null

Optional records

Records fields are always optional. Using the ?: optional notation throws an error.

const schema = compileSchema(`{
  record?: Record<string, string>;
}`);
// Throws: Type 'record' cannot be optional

About

Experimental schema builder using TypeScript template strings.

Topics

Resources

License

Stars

Watchers

Forks