Skip to content

Latest commit

 

History

History
1466 lines (1195 loc) · 37.9 KB

README.md

File metadata and controls

1466 lines (1195 loc) · 37.9 KB

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 .github/workflows/test.yml 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.