Wondering what’s next for npm?Check out our public roadmap! »

    tynder
    TypeScript icon, indicating that this package has built-in type declarations

    0.7.0 • Public • Published

    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(-11) 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(-1010) 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 Pstring[],
             QP | string,
             R{astring}[];

    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.

    Install

    npm i tynder

    DownloadsWeekly Downloads

    83

    Version

    0.7.0

    License

    ISC

    Unpacked Size

    3.32 MB

    Total Files

    270

    Last publish

    Collaborators

    • avatar