npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tynder

v0.7.0

Published

TypeScript friendly Data validator for JavaScript.

Downloads

396

Readme

Tynder

Tynder

TypeScript friendly Data validator for JavaScript.

Validate data in browsers, node.js back-end servers, and various language platforms by simply writing the schema once in TypeScript with extended syntax.

npm GitHub release Travis GitHub forks GitHub stars

Features

  • Define the schema with TypeScript-like DSL.
  • Validate data against the defined schema.
  • End user friendly custom validation error message.
  • Create subset by cherrypicking fields from original data with the defined schema.
  • Apply the patch data to the original data.
  • Generate type definition or schema files using CLI / API.
    • TypeScript
    • JSON Schema
    • C# (experimental)
    • Protocol Buffers 3 (experimental)
    • GraphQL (experimental)

write-once-use-anywhere


Table of contents


Get started

Playground

Install

npm install --save tynder

NOTICE:
Use with webpack >= 5

If you get the error:

Module not found: Error: Can't resolve '(importing/path/to/filename)'
in '(path/to/node_modules/path/to/dirname)'
Did you mean '(filename).js'?`

Add following setting to your webpack.config.js.

{
    test: /\.m?js/,
    resolve: {
        fullySpecified: false,
    },
},

On webpack >= 5, the extension in the request is mandatory for it to be fully specified if the origin is a '.mjs' file or a '.js' file where the package.json contains '"type": "module"'.

NOTICE:
To use without webpack on Node.js, enabling ES Modules.

  • Add flags:
    • node --experimental-modules \
           --es-module-specifier-resolution=node \
           --experimental-json-modules \
           app.mjs
  • Use import statement:
    • import { ValidationContext }       from 'tynder/modules/types';
      import { deserializeFromObject }   from 'tynder/modules/serializer';
      import { validate,
               getType }                 from 'tynder/modules/validator';
  • Add package.json { "type": "module" } or { "type": "commonjs" } to your source directories.

See tynder-express-react-ts-esm-quickstart and Node.js Documentation - ECMAScript Modules.

Define schema with TypeScript-like DSL

Schema:

/// @tynder-external RegExp, Date, Map, Set

/** doc comment */
export type Foo = string | number;

type Boo = @range(-1, 1) number;

/** doc comment */
interface Bar {
    /** doc comment */
    a?: string;                                                   // Optional field
    /** doc comment */
    b: Foo[] | null;                                              // Union type
    c: string[3..5];                                              // Repeated type (with quantity)
    d: (number | string)[..10];                                   // Complex repeated type (with quantity)
    e: Array<number | string, 4..>;                               // Complex repeated type (with quantity)
    f: Array<Array<Foo | string>>;                                // Complex repeated type (nested)
    g: [string, number],                                          // Sequence type
    h: ['zzz', ...<string | 999, 3..5>, number],                  // Sequence type (with quantity)
}

interface Baz {
    i: {x: number, y: number, z: 'zzz'} | number;                 // Union type
    j: {x: number} & ({y: number} & {z: number});                 // Intersection type
    k: ({x: number, y: number, z: 'zzz'} - {z: 'zzz'}) | number;  // Subtraction type
}

/** doc comment */
@msgId('M1111')                                                   // Custom error message id
export interface FooBar extends Bar, Baz {
    /** doc comment */
    @range(-10, 10)
    l: number;                                                    // Ranged value (number)
    @minValue(-10) @maxValue(10)
    m: number;                                                    // Ranged value
    n: @range(-10, 10) number[];                                  // Array of ranged value
    @greaterThan(-10) @lessThan(10)
    o: number;                                                    // Ranged value
    p: integer;                                                   // Integer value
    @range('AAA', 'FFF')
    q: string;                                                    // Ranged value (string)
    @match(/^.+$/)
    r: string;                                                    // Pattern matched value
    s: Foo;                                                       // Refer a defined type
    @msgId('M1234')
    t: number;                                                    // Custom error message id
    @msg({
        required: '"%{name}" of "%{parentType}" is required.',
        typeUnmatched: '"%{name}" of "%{parentType}" should be "%{expectedType}".',
    })
    u: number;                                                    // Custom error message
    @msg('"%{name}" of "%{parentType}" is not valid.')
    v: number;                                                    // Custom error message
}

// line comment
/* block comment */

Default file extension is *.tss.

Compile using CLI commands:

# Compile schema and output as JSON files.
tynder compile               --indir path/to/schema/tynder --outdir path/to/schema/_compiled
# Compile schema and output as JavaScript|TypeScript files.
tynder compile-as-ts         --indir path/to/schema/tynder --outdir path/to/schema/_compiled
# Compile schema and generate TypeScript type definition files.
tynder gen-ts                --indir path/to/schema/tynder --outdir path/to/typescript-src
# Compile schema and generate JSON Schema files.
tynder gen-json-schema       --indir path/to/schema/tynder --outdir path/to/schema/json-schema
# Compile schema and generate JSON Schema as JavaScript|TypeScript files.
tynder gen-json-schema-as-ts --indir path/to/schema/tynder --outdir path/to/schema/json-schema
# Compile schema and generate C# type definition files.
tynder gen-csharp            --indir path/to/schema/tynder --outdir path/to/schema/csharp
# Compile schema and generate Protocol Buffers 3 type definition files.
tynder gen-proto3            --indir path/to/schema/tynder --outdir path/to/schema/proto3
# Compile schema and generate GraphQL type definition files.
tynder gen-graphql           --indir path/to/schema/tynder --outdir path/to/schema/graphql

Compile using API:

import { compile } from 'tynder/modules/compiler';

export default const mySchema = compile(`
    type Foo = string;
    interface A {
        @maxLength(4)
        a: Foo;
        z?: boolean;
    }
`);

Validating:

import { validate,
         getType }           from 'tynder/modules/validator';
import { ValidationContext } from 'tynder/modules/types';
import default as mySchema   from './myschema';


const validated1 = validate({
    a: 'x',
    b: 3,
}, getType(mySchema, 'A')); // {value: {a: 'x', b: 3}}


const validated2 = validate({
    aa: 'x',
    b: 3,
}, getType(mySchema, 'A')); // null


const ctx3: Partial<ValidationContext> =
{                            // To receive the error messages, define the context as a variable.
    checkAll: true,          // (optional) Set to true to continue validation after the first error.
    noAdditionalProps: true, // (optional) Do not allow implicit additional properties.
    schema: mySchema,        // (optional) Pass "schema" to check for recursive types.
};

const validated3 = validate({
    aa: 'x',
    b: 3,
}, getType(mySchema, 'A'), ctx3);

if (validated3 === null) {
    console.log(JSON.stringify(
        ctx3.errors, // error messages
        null, 2));
}

Cherrypicking and patching:

import { getType }           from 'tynder/modules/validator';
import { pick,
         patch }             from 'tynder/modules/picker';
import { ValidationContext } from 'tynder/modules/types';
import * as op               from 'tynder/modules/operators';
import default as mySchema   from './myschema';


const original = {
    a: 'x',
    b: 3,
};
const needleType = op.picked(getType(mySchema, 'A'), 'a');


try {
    const needle1 = pick(original, needleType); // {a: 'x'}
    const unknownInput1: unknown = { // Edit the needle data
        ...needle1,
        a: 'y',
        q: 1234,
    };
    const changed1 = patch(original, unknownInput1, needleType); // {a: 'y', b: 3}
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}


try {
    const needle2 = pick(original, needleType); // {a: 'x'}
    const unknownInput2: unknown = { // Edit the needle data
        ...needle2,
        a: 'yyyyy',
        q: 1234,
    };
    const changed1 = patch(original, unknownInput2, needleType); // Throws an error
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}


try {
    const ctx3: Partial<ValidationContext> =
    {                     // To receive the error messages, define the context as a variable.
        checkAll: true,   // (optional) Set to true to continue validation after the first error.
        schema: mySchema, // (optional) Pass "schema" to check for recursive types.
    };

    const needle3 = pick({
        aa: 'x',
        b: 3,
    }, needleType, ctx3); // Throws an error
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}

Load pre-compiled schema and type definitions

From object (import)

...
import { deserializeFromObject } from 'tynder/modules/lib/serializer';
import { Foo, A }                from './path/to/schema-types/my-schema';    // type definitions (.d.ts)
import mySchema_,
       { Schema as MySchema }    from './path/to/schema-compiled/my-schema'; // pre-compiled schema (.ts)
                   // `MySchema` is auto generated string const enum.

const mySchema = deserializeFromObject(mySchema_);

const unknownInput: unknown = {a: 'x'};
const validated = validate<A>(unknownInput, getType(mySchema, MySchema.A));

if (validated) {
    const validatedInput = validated.value; // validatedInput is type-safe
    ...
}

From object (require JSON file)

...
import { deserializeFromObject } from 'tynder/modules/lib/serializer';
import { Foo, A }                from './path/to/schema-types/my-schema'; // type definitions (.d.ts)

// import { createRequireFromPath } from 'module';
// import { fileURLToPath }         from 'url';
// const require = createRequireFromPath(fileURLToPath(import.meta.url));

const mySchema = deserializeFromObject(
    require('./path/to/schema-compiled/my-schema.json')); // pre-compiled schema (.json)

const unknownInput: unknown = {a: 'x'};
const validated = validate<A>(unknownInput, getType(mySchema, 'A'));

if (validated) {
    const validatedInput = validated.value; // validatedInput is type-safe
    ...
}

or

...
import { deserializeFromObject } from 'tynder/modules/lib/serializer';
import { Foo, A }                from './path/to/schema-types/my-schema';         // type definitions (.d.ts)
import mySchemaJson              from './path/to/schema-compiled/my-schema.json'; // pre-compiled schema (.json)

const mySchema = deserializeFromObject(mySchemaJson);

const unknownInput: unknown = {a: 'x'};
const validated = validate<A>(unknownInput, getType(mySchema, 'A'));

if (validated) {
    const validatedInput = validated.value; // validatedInput is type-safe
    ...
}

From text

...
import { deserialize } from 'tynder/modules/lib/serializer';
import { Foo, A }      from './path/to/schema-types/my-schema'; // type definitions (.d.ts)
import * as fs         from 'fs';

const mySchema = deserialize(
    fs.readFileSync('./path/to/compiled/my-schema.json', 'utf8')); // pre-compiled schema (.json)

const unknownInput: unknown = {a: 'x'};
const validated = validate<A>(unknownInput, getType(mySchema, 'A'));

if (validated) {
    const validatedInput = validated.value; // validatedInput is type-safe
    ...
}

Type-safe Cherrypicking and patching:

// Load pre-compiled schema and type definitions
...

interface Store {
    baz: A;
}
const store: Store = {
    baz: {
        a: 'x',
        z: false,
    }
};

const needleType = op.picked(getType(mySchema, 'A'), 'a');

try {
    const needle = pick(store.baz, needleType); // {a: 'x'}
                                                // `needle` is RecursivePartial<A>
    const unknownInput: unknown = {             // Edit the needle data
        ...needle,
        a: 'y',
        q: 1234,
    };
    store.baz = patch(store.baz, unknownInput, needleType); // {a: 'y', z: false}
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}

Type guards

import { isType,
         getType } from 'tynder/modules/validator';

...

const unknownInput: unknown = {a: 'x'};

if (isType<A>(unknownInput, getType(mySchema, 'A'), ctx) && unknownInput.a.length > 0) {
    console.log(`ok: ${unknownInput.a.length}`);
} else {
    console.log('ng');
}
import { assertType,
         getType } from 'tynder/modules/validator';

...

const unknownInput: unknown = {a: 'x'};

try {
    assertType<A>(unknownInput, getType(mySchema, 'A'), ctx);
    console.log(`ok: ${unknownInput.a.length}`);
} catch (e) {
    console.log('ng');
}

Define schema with functional API

import { picked,
         omit,
         partial,
         intersect,
         oneOf,
         subtract,
         primitive,
         regexpPatternStringType,
         primitiveValue,
         optional,
         repeated,
         sequenceOf,
         spread,
         enumType,
         objectType,
         derived,
         symlinkType,
         withName,
         withTypeName,
         withDocComment,
         withRange,
         withMinValue,
         withMaxValue,
         withGreaterThan,
         withLessThan,
         withMinLength,
         withMaxLength,
         withMatch,
         withStereotype,
         withStereotype,
         withForceCast,
         withRecordType,
         withMeta,
         withMsg   as $$,
         withMsgId as $ } from 'tynder/modules/operators';

const myType =
    oneOf(
        derived(
            objectType(
                ['a', 10],
                ['b', optional(20)],
                ['c', $('MyType-c')(
                        optional('aaa'))],
                ['d', sequenceOf(
                        10, 20,
                        spread(primitive('string'), {min: 3, max: 10}),
                        50)], ),
            objectType(
                ['e', optional(primitive('string'))],
                ['f', primitive('string?')],
                ['g', repeated('string', {min: 3, max: 10})],
                [[/^[a-z][0-9]$/], optional(primitive('string'))], ),
            intersect(
                objectType(
                    ['x', 10], ['y', 10], ['p', 10], ),
                objectType(
                    ['x', 10], ['y', 10], ['q', 10], )),
            subtract(
                objectType(
                    ['w', 10], ['z', 10], ),
                objectType(
                    ['w', 10], ))),
        10, 20, 30,
        primitive('string'),
        primitiveValue(50), );

/*
Equivalent to following type definition:

interface P {
    e?: string;
    f?: string;
    g: string[3..10];
    [propName: /^[a-z][0-9]$/]?: string;
}
type Q = {
        x: 10, y: 10, p: 10,
    } & {
        x: 10, y: 10, q: 10,
    };
type R = {
        w: 10, z: 10,
    } - {
        w: 10,
    };
interface S extends P, Q, R {
    a: 10;
    b?: 20;
    @msgId('MyType-c')
    c: 'aaa';
    d: [10, 20, ...<string, 3..10>, 50];
}
type MyType = S | 10 | 20 | 30 | string | 50;
*/

const validated1 = validate({...}, myType);

DSL syntax

Type

type Foo = string;
type Bar = string[] | 10 | {a: boolean} | [number, string];

Interface

Named interface

interface Foo {
    a: string;   // Separators `;` and `,` are both allowed.
    b?: number;
}

interface Bar {
    c: boolean;
}

interface Baz extends Foo, Bar {
    d: string[];
}

Unnamed literal interface

type A = {
    a: string,   // Separators `;` and `,` are both allowed.
    b?: number,
};

Optional member

interface A {
    b?: number; // optional member
};

Additional properties

type X = {a: string, b: number};

interface A {
    // Additional properties (Error if `propName` is unmatched)
    [propName: string | number | /^[a-z][0-9]+$/]: number;
};

interface B {
    // Optional additional properties (Check type if propName matches)
    //   -> Implicit additional properties are allowed
    //      even if `ctx.noAdditionalProps` is true.
    [propName: string | number | /^[a-z][0-9]+$/]?: number; 
};

interface C {
    // `propName` can be any name
    [p: string]: X; 
};

interface D {
    // Error if app `propName`s are unmatched
    [propName1: /^[a-z][0-9]+$/]: number;
    [propName2: number]: number;
};

interface E {
    // If optional additional properties definition(s) exist,
    // implicit additional properties are allowed
    // even if `ctx.noAdditionalProps` is true.
    [propName1: /^[a-z][0-9]+$/]: number;
    [propName2: number]: number;
    [propName3: /^[A-F]+$/]?: number;
};

Only string, number, and RegExp are allowed for the propName type.

Type decoration

Decorate to interface member

interface A {
    @range(-10, 10) @msgId('M1234')
    a: number;
}

Decorate to type component

type A = @range(-10, -1) number | @range(1, 10) number;

interface B {
    b: @range(-10, -1) number | @range(1, 10) number;
}
  • @range(minValue: number | string, maxValue: number | string)
    • Check value range.
    • minValue <= data <= maxValue
  • @minValue(minValue: number | string)
    • Check value range.
    • minValue <= data
  • @maxValue(maxValue: number | string)
    • Check value range.
    • data <= maxValue
  • @greaterThan(minValue: number | string)
    • Check value range.
    • minValue < data
  • @lessThan(maxValue: number | string)
    • Check value range.
    • data < maxValue
  • @minLength(minLength: number)
    • Check value range.
    • minLength <= data.length
  • @maxLength(maxLength: number)
    • Check value range.
    • data.length <= maxLength
  • @match(pattern: RegExp)
    • Check value text pattern.
      • RegExp flags are allowed.
        • e.g.: /^[\u{3000}-\u{301C}]+$/u
    • pattern.test(data)
  • @stereotype(stereotype: string)
    • Perform custom validation.
      • WARNING: In the JSON schema output, this is stripped.

  • @constraint(constraintName: string, args: any)
    • Perform custom constraint.
      • WARNING: In the JSON schema output, this is stripped.

      • @constraint('unique', fields?: string[])
        • Check unique.
      • @constraint('unique-non-null', fields?: string[])
        • Check unique (null field is always unique).
      interface A {
          @constraint('unique')
          a: string[];
      }
      interface B {
          @constraint('unique', ['p', 'r'])
          b: {p: string, q: string, r: string}[];
      }
  • @forceCast
    • Validate after forcibly casting to the assertion's type.
      • WARNING: In the JSON schema output, this is stripped.

  • @recordType
    • If the decorated member field of object is validated, the union type is determined.
      • Use to receive reasonable validation error messages.
    interface Foo {
        @recordType kind: 'foo';
        ...
    }
    interface Bar {
        @recordType kind: 'bar';
        ...
    }
    type FooBar = Foo | Bar;
    // If data {kind: 'foo', ...} is passed,
    // the union type will be determined as `Foo`.
  • @meta
    • User defined custom properties (meta informations).
      • Output to the compiled schema.
    @meta({ objectId: '0ffc31e6-f534-4e49-b6d7-a3ec21f49637' })
    interface A {
        @meta({
            fieldId: '82bd5832-c399-4d4c-8bc4-b76a95823ebf',
            fieldType: 'checkbox',
        })
        a: ('foo' | 'bar' | 'baz')[];
    }
  • @msg(messages: string | ErrorMessages)
    • Set custom error message.
  • @msgId(messageId: string)
    • Set custom error message id.
Date / Datetime stereotypes
...
import { stereotypes as dateStereotypes } from 'tynder/modules/stereotypes/date';

const schema = compile(`
    interface Foo {
        @stereotype('date')
        @range('=today first-date-of-mo', '=today last-date-of-mo')
        a: string;

        @stereotype('date')
        @range('2020-01-01', '2030-12-31')
        b: string;

        @stereotype('date')
        @range('2020-01-01', '=today +2yr @12mo @31day')
        c: string;
    }
`);

const ty = getType(schema, 'Foo');
const ctx: Partial<ValidationContext> = {
    checkAll: true,
    stereotypes: new Map([
        ...dateStereotypes,
    ]),
};

const d = (new Date()).toISOString().slice(0, 10);

const z = validate<any>({
    a: d,
    b: '2020-01-01',
    c: d,
}, ty, ctx);
Stereotypes
  • date
    • date (UTC timezone)
  • lcdate
    • date (local timezone)
  • datetime
    • datetime (UTC timezone)
  • lcdatetime
    • datetime (local timezone)
Formula syntax
Expression =
    ISODateAndDatetime |
    ("=" , DateTimeFormula , {whitespace, DateTimeFormula}) ;

DateTimeFormula =
    ISODateAndDatetime |
    ("current" | "now") |
    "today"
    ("@" | "+" | "-") , NaturalNumber ,
            ("yr" | "mo"  | ("days" | "day") |
             "hr" | "min" | "sec" | "ms") |
    "first-date-of-yr" |
    "last-date-of-yr" |
    "first-date-of-mo" |
    "last-date-of-mo" |
    "first-date-of-fy", "(", NaturalNumber1To12, ")" ;
Formula examples
  • This month (date)
    • @range('=today first-date-of-mo', '=today last-date-of-mo')
  • This month (datetime)
    • @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')
  • Next month (date)
    • @range('=today first-date-of-mo +1mo', '=today @1day +1mo last-date-of-mo')
  • Next month (datetime)
    • @minValue('=today first-date-of-mo +1mo') @lessThan('=today @1day +1mo last-date-of-mo +1day')
  • This year (date)
    • @range('=today first-date-of-yr', '=today last-date-of-yr')
  • This year (datetime)
    • @minValue('=today first-date-of-yr') @lessThan('=today last-date-of-yr +1day')
  • Next year (date)
    • @range('=today first-date-of-yr +1yr', '=today @1day +1yr last-date-of-yr')
  • Next year (datetime)
    • @minValue('=today first-date-of-yr +1yr') @lessThan('=today @1day +1yr last-date-of-yr +1day')
  • This fiscal year (date)
    • @range('=today first-date-of-fy(9)', '=today first-date-of-fy(9) +1yr -1day')
      • Fiscal year beginning in September
  • This fiscal year (datetime)
    • @minValue('=today first-date-of-fy(9)') @lessThan('=today first-date-of-fy(9) +1yr')
      • Fiscal year beginning in September
  • Next fiscal year (date)
    • @range('=today first-date-of-fy(9) +1yr', '=today first-date-of-fy(9) +2yr -1day')
      • Fiscal year beginning in September
  • Next fiscal year (datetime)
    • @minValue('=today first-date-of-fy(9) +1yr') @lessThan('=today first-date-of-fy(9) +2yr')
      • Fiscal year beginning in September
Unique constraint
...
import { constraints as uniqueConstraints } from 'tynder/modules/constraints/unique';

const schema = compile(`
    interface A {
        @constraint('unique')
        a: string[];
    }
    interface B {
        @constraint('unique', ['p', 'r'])
        b: {p: string, q: string, r: string}[];
    }
`);

{
    const ty = getType(schema, 'A');
    const ctx: Partial<ValidationContext> = {
        checkAll: true,
        customConstraints: new Map([
            ...uniqueConstraints,
        ]),
    };
    const z = validate<any>({a: [
        'x',
        'y',
        'x', // duplicated
    ]}, ty, ctx);
}
{
    const ty = getType(schema, 'B');
    const ctx: Partial<ValidationContext> = {
        checkAll: true,
        customConstraints: new Map([
            ...uniqueConstraints,
        ]),
    };
    const z = validate<any>({a: [
        {p: '1', q: '2', r: '3'},
        {p: '2', q: '3', r: '4'},
        {p: '1', q: '4', r: '3'}, // duplicated
    ]}, ty, ctx);
}

Enum

enum Foo {
    A,  // 0
    B,  // 1
    C,  // 2
}

enum Bar {
    A = 1,    //   1
    B,        //   2
    C = 100,  // 100
}

enum Baz {
    A = 'AAA',
    B = 'BBB',
    C = 'CCC',
}

const enum Qux {
    A,
}

Primitive types

/** Primitive types */
type A = number | integer | bigint | string | boolean;

/** Null-like types */
type B = null | undefined;

/** Placeholder types */
type C = any | unknown | never;

Value types

See Literals > Type literals section.

Array type component (Repeated type component)

Simple array type

type A = string[];

Complex array type

type A = Array<boolean|number|boolean[]|{a: string}|'a'>;

Simple array type with quantity assertion

type A = string[10..20]; // 10 <= data.length <= 20
type B = string[10..];   // 10 <= data.length
type C = string[..20];   //       data.length <= 20
type D = string[10];     // data.length === 10

Complex array type with quantity assertion

type A = Array<boolean, 10..20>; // 10 <= data.length <= 20
type B = Array<boolean, 10..>;   // 10 <= data.length
type C = Array<boolean, ..20>;   //       data.length <= 20
type D = Array<boolean, 10>;     // data.length === 10

Sequence type component (Tuple type component)

Fixed length

type A = [string, number, 10, 20, 'a'];

Flex length

type A = [string, number?, boolean?, string?];              // Zero or once
type B = [string, ...<number>, ...<boolean>, ...<string>];  // Zero or more
type C = [string, ...<number, 10..20>,
                  ...<boolean, 10..>,
                  ...<string, ..20>];                       // With quantity assertion

WARNING: In the JSON schema output, this translates into a simplified array assertion.

Referencing other interface members

interface Foo {
    @match(/^[A-Za-z]+$/)
    name: string;
    @match(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)
    email: string;
}

interface Bar {
    foo: Foo
}

interface User {
    userName: Foo.name;
    primaryEmail: Foo.email;
    primaryAliasName: Bar.foo.name;
    aliasNames: Bar.foo.name[];
}

NOTE:

  • This syntax is incompatible with TypeScript.
    • Generated TypeScript type definition is userName: Foo['name'];.
    • Tynder compiler does not allow userName: Foo['name'];.

Type operators

  • P & Q
    • Intersection type
    • Result type has all the members of P and Q.
  • P | Q
    • Union type
    • Match to P or Q type.
  • P - Q
    • Subtraction type
    • Result type has the members of P that is NOT exist in Q.
  • Pick<T,K>
    • e.g. Pick<Foo, 'a' | 'b' | 'c'>
    • Picked type
    • Result type has the members of T that is exist in K.
  • Omit<T,K>
    • e.g. Omit<Foo, 'a' | 'b' | 'c'>
    • Picked type
    • Result type has the members of T that is NOT exist in K.
  • Partial<T>
    • All the member of result type are optioonal.
    • Partial<{a: string}> is equivalent to {a?: string}.

Export

export type Foo = string;

export interface Bar {
    a: string;
}

export enum Baz {
    A,
}

export const enum Qux {
    A,
}

Import

This statement is passed through to the generated codes.

import from 'foo';
import * as foo from 'foo';
import {a, b as bb} from 'foo';

Declared types

declare type A = string;
declare interface B {}
declare enum C {}
declare const enum D {}

export declare type E = string;
export declare interface F {}
export declare enum G {}
export declare const enum H {}

Declared variables

This statement is passed through to the generated codes.

declare var a: number;
declare let b: number;
declare const c: number;

export declare var d: number;
export declare let e: number;
export declare const f: number;

External

This statement is removed from the generated code.

Untyped external statement

Define the external (ambient) symbols as any type.

external P, Q, R;

or

/// @tynder-external P, Q, R

or

/* @tynder-external P, Q, R */

Typed external statement

external P: string[],
         Q: P | string,
         R: {a: string}[];

or

/// @tynder-external P: string[], Q: P | string, R: {a: string}[]

or

/* @tynder-external
    P: string[],
    Q: P | string,
    R: {a: string}[]
*/

Pass-through code block

This comment body is passed through to the generated codes.

// Nominal type

declare const phoneNumberString: unique symbol;
/* @tynder-pass-through
export type PhoneNumberString = string & { [phoneNumberString]: never };
*/
external PhoneNumberString: @match(/^[0-9]{2,4}-[0-9]{1,4}-[0-9]{4}$/) string;

Comments

//  ↓↓↓ directive line comment ↓↓↓
// @tynder-external P, Q, R
/// @tynder-external S, T

//  ↓↓↓ directive block comment ↓↓↓
/* @tynder-external U, V */


/** doc comment */
type Foo = string | number;

/** doc comment */
interface Bar {
    /** doc comment */
    a?: string;
}

/** doc comment */
enum Baz {
    /** doc comment */
    A,
}

// line comment
# line comment

/* block comment */
/*
   block comment
 */

Doc comments are preserved.

Literals

Type literals

type A = 'a' | "b" | `c` |
         20 | -10 | -0.12 | -9.3+8e |
         -10_000_000.999_999 |
         0xff | 0o77 | 0b11 | +Infinity | -Infinity |
         -10n | 0n | 123n |
         true | false | null | undefined |
         {a: string, b: 'aaa'} | [10, string];

Value literals

type A = @match(/^.+$/) string;     // RegExp
type B = @range(10, 20) number;     // number
type C = @range('a', 'b') string;   // string
type D = @msg({
    required: '...',
    typeUnmatched: '...' }) number; // object

Directives

/// @tynder-external P, Q, R
  • @tynder-external type [, ...]
    • Declare external types as any.
/* @tynder-pass-through
export type PhoneNumberString = string & { [phoneNumberString]: never };
*/
  • @tynder-pass-through body
    • This comment body is passed through to the generated codes.

Generics

Generics actual parameters are removed.

DSL:

/// @tynder-external Map, Set

interface Foo {
    a: Map<string, number>;  // validator treats it as `any`.
    b: Set<string>;          // validator treats it as `any`.
}

TypeScript generated type definition:

interface Foo {
    a: Map;  // generics actual parameters are removed.
    b: Set;  // generics actual parameters are removed.
}

NOTE: Generic interfaces and generic types cannot be defined.

  • e.g.

    interface Foo<T> { // It is not possible.
        a: T;
    }

Customize error messages

Customize message of items

@msgId('M1111')                                                   // Custom error message id
export interface Foo {
    @msgId('M1234')
    s: number;                                                    // Custom error message id

    @msg({
        required: '"%{name}" of "%{parentType}" is required.',
        typeUnmatched: '"%{name}" of "%{parentType}" should be "%{expectedType}".',
    })
    t: number;                                                    // Custom error message

    @msg('"%{name}" of "%{parentType}" is not valid.')
    u: number;                                                    // Custom error message
}

Default error messages

export const defaultMessages: ErrorMessages = {
    invalidDefinition:       '"%{name}" of "%{parentType}" type definition is invalid.',
    required:                '"%{name}" of "%{parentType}" is required.',
    typeUnmatched:           '"%{name}" of "%{parentType}" should be type "%{expectedType}".',
    additionalPropUnmatched: '"%{addtionalProps}" of "%{parentType}" are not matched to additional property patterns.',
    repeatQtyUnmatched:      '"%{name}" of "%{parentType}" should repeat %{repeatQty} times.',
    sequenceUnmatched:       '"%{name}" of "%{parentType}" sequence is not matched',
    valueRangeUnmatched:     '"%{name}" of "%{parentType}" value should be in the range %{minValue} to %{maxValue}.',
    valuePatternUnmatched:   '"%{name}" of "%{parentType}" value should be matched to pattern "%{pattern}"',
    valueLengthUnmatched:    '"%{name}" of "%{parentType}" length should be in the range %{minLength} to %{maxLength}.',
    valueUnmatched:          '"%{name}" of "%{parentType}" value should be "%{expectedValue}".',
};

Change default messages

import { compile }           from 'tynder/modules/compiler';
import { getType }           from 'tynder/modules/validator';
import { pick,
         merge }             from 'tynder/modules/picker';
import { ValidationContext } from 'tynder/modules/types';

export default const mySchema = compile(`
    interface A {
        @msg({
            required: 'Don\'t forget "%{name}"!.',
        })
        a: string;
    }
`);

const ctx: Partial<ValidationContext> = {
    checkAll: true,
    noAdditionalProps: true,
    schema: mySchema,
    errorMessages: {
        required: '%{name}" is requred!',
    },
};

const validated = validate({
    aa: 'x',
}, getType(mySchema, 'A'), ctx3);

if (validated3 === null) {
    console.log(JSON.stringify(
        ctx3.errors, // error messages
        null, 2));
}

Precedence is "Default messages < ctx.errorMessages < @msg()".

Keyword substitutions

  • %{expectedType}
  • %{type}
  • %{expectedValue}
  • %{value}
  • %{repeatQty}
  • %{minValue}
  • %{maxValue}
  • %{pattern}
  • %{minLength}
  • %{maxLength}
  • %{name}
  • %{parentType}
  • %{dataPath}
  • %{addtionalProps}

CLI subcommands and options

Usage:
  tynder subcommand options...

Subcommands:
  help
      Show this help.
  compile
      Compile schema and output as JSON files.
          * default input file extension is *.tss
          * default output file extension is *.json
  compile-as-ts
      Compile schema and output as JavaScript|TypeScript files.
          * default input file extension is *.tss
          * default output file extension is *.ts
      Generated code is:
          const schema = {...};
          export default schema;
  gen-ts
      Compile schema and generate TypeScript type definition files.
          * default input file extension is *.tss
          * default output file extension is *.d.ts
  gen-json-schema
      Compile schema and generate 'JSON Schema' files.
          * default input file extension is *.tss
          * default output file extension is *.json
  gen-json-schema-as-ts
      Compile schema and generate 'JSON Schema'
      as JavaScript|TypeScript files.
          * default input file extension is *.tss
          * default output file extension is *.ts
      Generated code is:
          const schema = {...};
          export default schema;
  gen-csharp
      Compile schema and generate 'C#' type definition files.
          * default input file extension is *.tss
          * default output file extension is *.cs
  gen-proto3
      Compile schema and generate 'Protocol Buffers 3' type definition files.
          * default input file extension is *.tss
          * default output file extension is *.proto
  gen-graphql
      Compile schema and generate 'GraphQL' type definition files.
          * default input file extension is *.tss
          * default output file extension is *.graphql

Options:
  --indir dirname
      Input directory
  --outdir dirname
      Output directory
  --inext fileExtensionName
      Input files' extension
  --outext fileExtensionName
      Output files' extension

Example:

tynder compile --indir path/to/schema/tynder --outdir path/to/schema/_compiled

Limitations

License

ISC
Copyright (c) 2019-2020 Shellyl_N and Authors.