• Stars
    star
    109
  • Rank 319,077 (Top 7 %)
  • Language
    TypeScript
  • License
    Other
  • Created almost 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

TypeScript friendly Data validator for JavaScript.

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.

More Repositories

1

kanban-board-app

Kanban style task management board app
TypeScript
135
star
2

chart.js-node-ssr-example

Chart.js server side rendering example. (pure JavaScript; no native modules)
TypeScript
45
star
3

vue-electron-typescript-quickstart

A boilerplate of Electron app that uses Vue in TypeScript.
TypeScript
30
star
4

liyad

Liyad (Lisp yet another DSL interpreter) is very small Lisp interpreter written in JavaScript.
TypeScript
30
star
5

red-agate

Static HTML | XML | SVG renderer using JSX, suitable for report output.
TypeScript
20
star
6

open-soql

Open source implementation of the SOQL.
TypeScript
16
star
7

takenoco

A parser combinator library for Go.
Go
9
star
8

menneu

Component-based extensible document processor
TypeScript
7
star
9

kdx

kintone CLI for development & deployment, with Developer Experience.
TypeScript
6
star
10

dust-lang

Toy scripting language with a syntax similar to Rust.
Go
5
star
11

vue-electron-typescript-grpc-quickstart

A boilerplate of Electron app that uses Vue in TypeScript. Plus, this app implements gRPC client and spawn the child process which is a gRPC server.
TypeScript
5
star
12

fruitsconfits

A well typed and sugared parser combinator framework for TypeScript/JavaScript.
TypeScript
5
star
13

mdne

Markdown Neo Edit - A simple markdown and code editor powered by Markdown-it, Ace and Carlo.
JavaScript
5
star
14

iCalForce

[DEPRECATED] iCalendar (.ics) exporter for Salesforce/Force.com. You can watch Salesforce's "Event" via Google calendar, Outlook.com,...
PHP
4
star
15

mdne-for-kintone

mdne for kintone - Edit kintone fields with powerful markdown and code editor.
JavaScript
4
star
16

knockout-webpack-ts-quickstart

Quickstart project for knockout.js + TypeScript with Webpack2.
JavaScript
4
star
17

webpack-typescript-lib-quickstart

[DEPRECATED] Quickstart project scaffolding for TypeScript library that runs on browsers and/or Node build with Webpack 2.
JavaScript
4
star
18

mdne-sf

Markdown Neo Edit for Salesforce - A simple markdown and code editor powered by Markdown-it and Ace.
JavaScript
3
star
19

go-sql-like-expr

Convert SQL Like patterns to Go regular expressions.
Go
3
star
20

matrixcode.js-legacy

[DEPRECATED] QR Code, Data Matrix, PDF417, and other barcodes library for JavaScript (experimental implementation, not tested well)
JavaScript
3
star
21

liyad-cli

CLI and REPL for Liyad (Lisp yet another DSL interpreter)
JavaScript
2
star
22

menneu-reporting-app-for-kintone

Create ✨beautiful✨ 📑📊reports📈📰 easily with Ménneu + kintone.
JavaScript
2
star
23

go-roleplay-chatbot

Chatbot using OpenAI GPT-3.5-turbo / GPT-4
Go
2
star
24

open-soql-usage-example

Usage example of Open SOQL.
JavaScript
2
star
25

go-loose-json-parser

Super loose JSON + TOML parsers and unmarshaller for Go. JSONC and JSON5 can also be read.
Go
2
star
26

tynder-chrome-extension

Tynder TypeScript to JSON Schema, etc... schema converter Chrome extension
JavaScript
2
star
27

out-of-proc-server

Out-of-proc-server (+ gRPC) provides interoperability between Node.js and .NET framework.
JavaScript
2
star
28

wp-quickstart-caller-example

[DEPRECATED] Usage example of webpack-typescript-lib-quickstart. Quickstart project scaffolding for TypeScript library that runs on browsers and/or Node build with Webpack 2.
JavaScript
2
star
29

go-open-soql-visualizer

SOQL query visualizer powered by go-open-soql-parser and Mermaid.
Go
2
star
30

go-graphdt

A datatable that represents object graphs for Go.
Go
2
star
31

go-open-soql-parser

Open source implementation of the SOQL parser for Go
Go
1
star
32

mdne-electron

Markdown Neo Edit for Electron - A simple markdown and code editor powered by Markdown-it, Ace and Electron.
JavaScript
1
star
33

red-agate-example

RedAgate usage examples
TypeScript
1
star
34

tynder-express-react-ts-esm-quickstart

A boilerplate for React client + Express server project using Tynder data validation library.
TypeScript
1
star
35

sf-vf-slds-for-react-boilerplate

Salesforce "Visualforce" + "Lightning Design System for React" boilerplate 🚀
JavaScript
1
star
36

liyad-webapp-example

NewLisp
1
star
37

kanban-board-for-kintone

Kanban board for kintone
Common Lisp
1
star
38

kanban-board-calendar-exporter

Export kanban-board-app tasks as iCal (*.ics) format with AWS Lambda + S3 infrastructure.
JavaScript
1
star
39

red-agate-live-demo

RedAgate Live Demo
HTML
1
star
40

go-single-bin-static-web-server

Template for a single-binary web server written in Go
Go
1
star
41

red-agate-svg-canvas

[Repositories are unified to https://github.com/shellyln/red-agate] RedAgate's SVG Canvas shared library
TypeScript
1
star
42

go-small-jsonpath

Small, feature limited JSONPath (+dialect) implementation.
Go
1
star
43

liyad-lisp-pkg-example

Common Lisp
1
star
44

red-agate-barcode

[Repositories are unified to https://github.com/shellyln/red-agate] RedAgate's 1d/2d barcodes shared library
TypeScript
1
star
45

go-nameutil

Makefile
1
star
46

zirconia

Report rendering library for Salesforce LWC and Visualforce
JavaScript
1
star
47

kdx-project-template

Project template for kintone KDX CLI.
TypeScript
1
star
48

menneu-api-usage-on-esm

Usage example of Ménneu API on ES modules
JavaScript
1
star
49

open-soql-react-hooks-example-app

TypeScript
1
star
50

red-agate-util

[Repositories are unified to https://github.com/shellyln/red-agate] RedAgate's utilities shared library
TypeScript
1
star
51

menneu-md-notebook

Ménneu Markdown Notebook - Edit markdown locally w/o installing any apps.
HTML
1
star
52

red-agate-math

[Repositories are unified to https://github.com/shellyln/red-agate] RedAgate's math (Finite field, Error correction (RS,BCH,CRC)) shared library
TypeScript
1
star