Git Product home page Git Product logo

typescript-is's Introduction

Deprecation Notice

typescript-is is deprecated. It still works with TS 4.8 below, but it will not be updated.

For TypeScript v4.8+, please use typia instead.

typescript-is

TypeScript transformer that generates run-time type-checks.

npm node Travis (.org) npm David David NpmLicense

๐Ÿ’ฟ Installation

npm install --save typescript-is

# Ensure you have the required dependencies at compile time:
npm install --save-dev typescript

# If you want to use the decorators, ensure you have reflect-metadata in your dependencies:
npm install --save reflect-metadata

๐Ÿ’ผ Use cases

If you've worked with TypeScript for a while, you know that sometimes you obtain any or unknown data that is not type-safe. You'd then have to write your own function with type predicates that checks the foreign object, and makes sure it is the type that you need.

This library automates writing the type predicate function for you.

At compile time, it inspects the type you want to have checked, and generates a function that can check the type of a wild object at run-time. When the function is invoked, it checks in detail if the given wild object complies with your favorite type.

In particular, you may obtain wild, untyped object, in the following situations:

  • You're doing a fetch call, which returns some JSON object. You don't know if the JSON object is of the shape you expect.
  • Your users are uploading a file, which is then read by your application and converted to an object. You don't know if this object is really the type you expect.
  • You're reading a JSON string from localStorage that you've stored earlier. Perhaps in the meantime the string has been manipulated and is no longer giving you the object you expect.
  • Any other case where you lose compile time type information...

In these situations typescript-is can come to your rescue.

NOTE this package aims to generate type predicates for any serializable JavaScript object. Please check What it won't do for details.

Similar projects

๐ŸŽ›๏ธ Configuration

This package exposes a TypeScript transformer factory at typescript-is/lib/transformer-inline/transformer

As there currently is no way to configure the TypeScript compiler to use a transformer without using it programatically, the recommended way is to compile with ttypescript. This is basically a wrapper around the TypeScript compiler that injects transformers configured in your tsconfig.json.

(please vote here to support transformers out-of-the-box: microsoft/TypeScript#14419)

Using ttypescript

First install ttypescript:

npm install --save-dev ttypescript

Then make sure your tsconfig.json is configured to use the typescript-is transformer:

{
    "compilerOptions": {
        "plugins": [
            { "transform": "typescript-is/lib/transform-inline/transformer" }
        ]
    }
}

Now compile using ttypescript:

npx ttsc

Using with ts-node, webpack, Rollup

Please check the README of ttypescript for information on how to use it in combination with ts-node, webpack, and Rollup.

Note: This will not work if ts-loader is configured with transpileOnly: true.

Using with webpack + ts-loader without ttypescript

If you are using ts-loader in a webpack project, you can use getCustomTransformers as suggested in #54. This means you don't need to use ttypescript or write a custom compilation script.

Example:

const typescriptIsTransformer = require('typescript-is/lib/transform-inline/transformer').default

module.exports = {
    // I am hiding the rest of the webpack config
    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'ts-loader',
                options: {
                    getCustomTransformers: program => ({
                        before: [typescriptIsTransformer(program)]
                    })
                }
            }
        ]
    }
};

Note: This will not work if ts-loader is configured with transpileOnly: true.

Options

There are some options to configure the transformer.

Property Description
shortCircuit Boolean (default false). If true, all type guards will return true, i.e. no validation takes place. Can be used for example in production deployments where doing a lot of validation can cost too much CPU.
ignoreClasses Boolean (default: false). If true, when the transformer encounters a class (except for Date), it will ignore it and simply return true. If false, an error is generated at compile time.
ignoreMethods Boolean (default: false). If true, when the transformer encounters a method, it will ignore it and simply return true. If false, an error is generated at compile time.
ignoreFunctions (deprecated, use functionBehavior instead) Boolean (default: false). If true, when the transformer encounters a function, it will ignore it and simply return true. If false, an error is generated at compile time.
functionBehavior One of error, ignore, or basic (default: error). Determines the behavior of transformer when encountering a function. error will cause a compile-time error, ignore will cause the validation function to always return true, and basic will do a simple function-type-check. Overrides ignoreFunctions.
disallowSuperfluousObjectProperties Boolean (default: false). If true, objects are checked for having superfluous properties and will cause the validation to fail if they do. If false, no check for superfluous properties is made.
transformNonNullExpressions Boolean (default: false). If true, non-null expressions (eg. foo!.bar) are checked to not be null or undefined
emitDetailedErrors Boolean or auto (default: auto). The generated validation functions can return detailed error messages, pointing out where and why validation failed. These messages are used by assertType<T>(), but are ignored by is<T>(). If false, validation functions return empty error messages, decreasing code size. auto will generate detailed error messages for assertions, but not for type checks. true will always generate detailed error messages, matching the behaviour of version 0.18.3 and older.

If you are using ttypescript, you can include the options in your tsconfig.json:

{
    "compilerOptions": {
        "plugins": [
            {
                "transform": "typescript-is/lib/transform-inline/transformer",
                "shortCircuit": true,
                "ignoreClasses": true,
                "ignoreMethods": true,
                "functionBehavior": "ignore",
                "disallowSuperfluousObjectProperties": true,
                "transformNonNullExpressions": true,
                "emitDetailedErrors": "auto"
            }
        ]
    }
}

โญ How to use

Before using, please make sure you've completed configuring the transformer.

In your TypeScript code, you can now import and use the type-check function is (or createIs), or the type assertion function assertType (or createAssertType).

Validation (is and createIs)

For example, you can check if something is a string or number and use it as such, without the compiler complaining:

import { is } from 'typescript-is';

const wildString: any = 'a string, but nobody knows at compile time, because it is cast to `any`';

if (is<string>(wildString)) { // returns true
    // wildString can be used as string!
} else {
    // never gets to this branch
}

if (is<number>(wildString)) { // returns false
    // never gets to this branch
} else {
    // Now you know that wildString is not a number!
}

You can also check your own interfaces:

import { is } from 'typescript-is';

interface MyInterface {
    someObject: string;
    without: string;
}

const foreignObject: any = { someObject: 'obtained from the wild', without: 'type safety' };

if (is<MyInterface>(foreignObject)) { // returns true
    const someObject = foreignObject.someObject; // type: string
    const without = foreignObject.without; // type: string
}

Assertions (assertType and createAssertType)

Or use the assertType function to directly use the object:

import { assertType } from 'typescript-is';

const object: any = 42;
assertType<number>(object).toFixed(2); // "42.00"

try {
    const asString = assertType<string>(object); // throws error: object is not a string
    asString.toUpperCasse(); // never gets here
} catch (error) {
    // ...
}

Decorators (ValidateClass and AssertType)

You can also use the decorators to automate validation in class methods. To enable this functionality, you should make sure that experimental decorators are enabled for your TypeScript project.

{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

You should also make sure the peer dependency reflect-metadata is installed.

npm install --save reflect-metadata

You can then use the decorators:

import { ValidateClass, AssertType } from 'typescript-is';

@ValidateClass()
class A {
    method(@AssertType() value: number) {
        // You can safely use value as a number
        return value;
    }
}

new A().method(42) === 42; // true
new A().method('42' as any); // will throw error

async and Promise returning methods

AssertType can also work correctly with async methods, returning promise rejected with TypeGuardError

To enable this functionality, you need to emit decorators metadata for your TypeScript project.

{
    "compilerOptions": {
      "emitDecoratorMetadata": true
    }
}

Then AssertType will work with async methods and Promise returning methods automatically.

import { ValidateClass, AssertType } from 'typescript-is';

@ValidateClass()
class A {
    async method(@AssertType({ async: true }) value: number) {
        // You can safely use value as a number
        return value;
    }

    methodPromise(@AssertType({ async: true }) value: number): Promise<number> {
        // You can safely use value as a number
        return Promise.resolve(value);
    }
}

new A().method(42).then(value => value === 42 /* true */); 
new A().method('42' as any).catch(error => {
    // error will be of TypeGuardError type
})
new A().methodPromise('42' as any).catch(error => {
    // error will be of TypeGuardError type
})

If you want to throw synchronously for some reason, you can override the behaviour using with @AssertType({ async: false }):

import { ValidateClass, AssertType } from 'typescript-is';

@ValidateClass()
class A {
    async method(@AssertType({ async: false }) value: number) {
        // You can safely use value as a number
        return value;
    }
}

new A().method(42).then(value => value === 42 /* true */);
new A().method('42' as any); // will throw error

If you cannot or don't want to enable decorators metadata, you still make AssertType reject with promise using @AssertType({ async: true })

import { ValidateClass, AssertType } from 'typescript-is';

@ValidateClass()
class A {
    async method(@AssertType({ async: true }) value: number) {
        // You can safely use value as a number
        return value;
    }
}

Strict equality (equals, createEquals, assertEquals, createAssertEquals)

This family of functions check not only whether the passed object is assignable to the specified type, but also checks that the passed object does not contain any more than is necessary. In other words: the type is also "assignable" to the object. This functionality is equivalent to specifying disallowSuperfluousObjectProperties in the options, the difference is that this will apply only to the specific function call. For example:

import { equals } from 'typescript-is';

interface X {
    x: string;
}

equals<X>({}); // false, because `x` is missing
equals<X>({ x: 'value' }); // true
equals<X>({ x: 'value', y: 'another value' }); // false, because `y` is superfluous

To see the declarations of the functions and more examples, please check out index.d.ts.

For many more examples, please check out the files in the test/ folder. There you can find all the different types that are tested for.

โ›” What it won't do

  • This library aims to be able to check any serializable data.
  • This library will not check functions. Function signatures are impossible to check at run-time.
  • This library will not check classes (except the global Date). Instead, you are encouraged to use the native instanceof operator. For example:
import { is } from 'typescript-is';

class MyClass {
    // ...
}

const instance: any = new MyClass();
is<MyClass>(instance); // error -> classes are not supported.

// Instead, use instanceof:
if (instance instanceof MyClass) {
    // ...
}
  • This library will not magically check unbound type parameters. Instead, make sure all type parameters are bound to a well-defined type when invoking the is function. For example:
import { is } from 'typescript-is';

function magicalTypeChecker<T>(object: any): object is T {
    return is<T>(object); // error -> type `T` is not bound.
}

If you stumble upon anything else that is not yet supported, please open an issue or submit a PR. ๐Ÿ˜‰

๐Ÿ—บ๏ธ Road map

Features that are planned:

  • Promise support. Something like assertOrReject<Type>(object) will either resolve(object) or reject(error).
  • Optimize the generated conditions. Things like false || "key" === "key" can be simplified. Might be more interesting to publish a different library that can transform a TypeScript AST, and then use it here, or use an existing one. Might be out of scope, as there are plenty of minifiers/uglifiers/manglers out there already.

๐Ÿ”จ Building and testing

git clone https://github.com/woutervh-/typescript-is.git
cd typescript-is/
npm install

# Building
npm run build

# Testing
npm run test

typescript-is's People

Contributors

andykais avatar b-zurg avatar cauthmann avatar davidfurey avatar detachhead avatar dko-slapdash avatar gm-alex avatar jarofghosts avatar lukas-valenta avatar mitko-slapdash avatar oleg-slapdash avatar samchon avatar t-winter avatar tomas-sereikis avatar tsiege avatar woutervanheeswijk-tomtom avatar woutervh- avatar zdila avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typescript-is's Issues

Unable to use `Date` type in interfaces that will be validated

While using this library I have run into an issue where any interface I would like to be validated cannot hold any properties that hold a Date type. I have both "ignoreClasses": true and "ignoreMethods": true in my tsconfig.json. This problem occurs at run time, as validations with Date types fail every time, though it compiles just fine.

Glancing through the generated code, it seems a large list of checks is created for each Date typed property, that checks that it has a number of properties, including properties that aren't always supported. I found the first place that fails is a check that a Date has the getVarDate() method, which is only supported in Internet Explorer and obsolete according to MDN. I have not checked the rest. I am using this on server application for cursory incoming type checking.

Right now my workaround is to have a separate interface without any Date properties simply for use with validation and then checking for those properties manually. This would not work with the setting "disallowSuperfluousObjectProperties": true in the tsconfig.json.

NestedException on transforming a type

It might be easier to read from the bottom as I simplified and narrowed down the source of the error over time

I get the following exception having updated a type. The update is non-trivial, and I've been unable (so far) to pin down the exact cause, but here's the jist of what the type is.

It represents a set of jobs, each of which is an async function with either parameters in an array via a params member, or a single parameter via a param member, like this:

interface ValidJobs {
  testJob: typeof testJob,
  rolloverJob: typeof rolloverJob,
  replicationTask: typeof replicationTask,
  processOutstanding: typeof processOutstanding,
}

type GenericJobFunction = (...args: any[]) => Promise<any>;
type JobParams<J extends GenericJobFunction> = Parameters<J> extends { length: 1 }
  ? { params: Parameters<J> } | { param: Parameters<J>[0] }
  : Parameters<J> extends { length: 0 } 
    ? undefined 
    : { params: Parameters<J> }

type JobSpec<J extends GenericJobFunction> = {
  runsOn?: Roles[] | undefined | null | false,
  interval: number | 'startup' | 'never'; 
  timeout?: number; 
} & JobParams<J>

export type Jobs = {
  [jobName in keyof ValidJobs]?: JobSpec<ValidJobs[jobName]> | false;
};

A sample of the object passed to validate is something like:

{
  "rolloverJob": {
    "interval":3,
    "param":{"key":"837hdy487srt56x" }
  },
  "replicationTask": {
    "interval":10,
    "params":[12,"hourly check"]
  },
  "processOutstanding": {
    "interval":5,
    "runsOn": ["masternodes"]
  },
}

The previous version of the type was much less strict about defining the relationship between the keys of Jobs and the parameters (ie it was possible to pass parameters for one job to the definition of another).

Obviously, if I do find what causes/suppresses the error I'll let you know, but I'm hoping the stack dump & defns will point you (and therefore me!) in the right direction.

Version: "typescript-is": "^0.13.0"

NestedError: Failed to transform node at: /Users/matthew.woolf/git/news-search/f-extract/config/index.ts:175:13
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:33:15)
    at ts.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNode (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67586:23)
    at Object.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67814:45)
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
    at ts.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNode (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67586:23)
    at Object.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67846:52)
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
    at ts.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
Caused By: Error: Could not generate type-check; unsupported type with flags: 16777216
    at visitType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:369:15)
    at ts.createFunctionDeclaration.ts.createBlock.propertyInfos.map (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:100:23)
    at Array.map (<anonymous>)
    at VisitorUtils.setFunctionIfNotExists (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:94:30)
    at Object.setFunctionIfNotExists (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-utils.js:34:46)
    at visitRegularObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:76:25)
    at visitObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:168:16)
    at visitType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:346:16)
    at intersectionType.types.map (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:202:72)
    at Array.map (<anonymous>)

JSON Schema generation?

In addition to the current functionality, it would be very nice to have JSON schema generation as part of the transform. This would provide a means of communicating types to other non-typescript systems. For example, the ability to perform client side validation of data by pushing out JSON schema to the clients so they could do their own local validation prior to submitting data to an HTTP API, for example.

Runtime issues when used with Jest

I've made the changes to jest.config.js to use ttypescript and I've modified my tsconfig.json to use typescript-is/lib/transform-inline/transformer as a transform plugin. I can compile everything fine. But when I try to use Jest, I get:

This module should not be used in runtime. Instead, use a transformer during compilation.

I've created a repo that reproduces the issue here:

https://github.com/xogeny/siren-types

Be sure to use the ttypescript branch.

More detailed error message for "Xyz | null" cases (not just "there are no valid alternatives")

Adding | null alternative significantly degrades verboseness of the error message.

Example:

createAssertType<{
  data: {
    node: { a: string; closed: string; b: string } | null;
  };
}>()({ data: { node: { a: "a", closed: false, b: "b" } } });

// Result:
// TypeGuardError: validation failed at $.data.node: there are no valid alternatives

Notice that it doesn't mention the name of the mis-typed field ("closed"), so it's pretty hard to debug, what's the problem (especially when there is a lot of fields).

If I remove | null clause, it starts showing the field name:

createAssertType<{
  data: {
    node: { a: string; closed: string; b: string };
  };
}>()({ data: { node: { a: "a", closed: false, b: "b" } } });

// Result:
// TypeGuardError: validation failed at $.data.node.closed: expected a string

Is it (at least theoretically) possible to improve error messages here? The case when | null or | undefined or field?: type are used is pretty common (e.g. in GraphQL responses, the field is often times nullable).

ignoreMethods has now effect

Using ignoreMethods has no effect for the following data

const test = {
    list: {
        listItem: "aaa",
        data: {
            id: {
                some: "span.to-details",
                convert: x => x
            }
        }
    }
};

validate against the following interface

interface IElement {
    some?: string;
    convert?: (value: string) => string;
}

interface IMap {
    id: string|IElement;
}

interface ITest {
    list: {
        listItem: string;
        data: IMap;
        convert?: (value: string) => string;
    };
}

by using assertEquals<ITest>(test). This results in TypeGuardError: validation failed at $.list.data.id: there are no valid alternatives. The following data results in TypeGuardError: validation failed at $.list.convert: expected an object

const test = {
    list: {
        listItem: "aaa",
        data: {
            id: {
                some: "span.to-details"
            }
        },
        convert: x => x
    },
};

Using

const test = {
    list: {
        listItem: "aaa",
        data: {
            id: {
                some: "span.to-details"
            }
        }
    },
};

works as expected.

Nested generic types result in "Unbound type parameter, missing type node."

Not sure if this is intended behavior, please let me know if so!

ttypescript version: 3.2.4
typescript-is version: 0.7.17
node version: 11.6.0

Steps to reproduce:
Try compiling a file containing the following:

import { is } from 'typescript-is';

interface Wrapped<T> {
    wrap: T;
}

interface X<T> {
    a: Wrapped<T>;
}

is<X<number>>({ a: { wrap: 2 } });

Actual results:
The compilation fails with an error like this:

typescript-is: transforming program with 30 source files; using TypeScript 3.2.4.

undefined:86995
                throw e;
                ^
NestedError: Failed to transform node at: /home/geomaster/Projects/tests/typescript-is/index.ts:9:2
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:27:15)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
    at visitNode (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63390:23)
    at Object.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63664:59)
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
    at visitNodes (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63441:48)
    at visitLexicalEnvironment (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63474:22)
    at Object.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63778:54)
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
Caused By: Error: Unbound type parameter, missing type node.
    at visitTypeParameter (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:255:15)
    at visitType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:362:16)
    at visitTypeParameter (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:257:12)
    at visitType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:362:16)
    at createPropertyCheck (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:8:28)
    at visitPropertySignature (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:41:16)
    at visitDeclaration (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:49:16)
    at visitPropertySymbol (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:77:25)
    at visitRegularObjectType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:131:29)
    at visitObjectType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:172:16)

Expected results:
Since the T type parameter is bound to number in X<number>, I'd expect wrap to also be of known type, precisely, Wrapped<number>. However, it looks like typescript-is isn't able to deduce this, and so thinks that T in a: Wrapped<T> is an unbound type parameter.

Thanks and let me know if I can help with more info!

Question - regexes on strings...?

Hi there, been playing about with this library and I think it looks really good - especially as a way of defining contracts for messages between client / server.

This is probably out of scope but I thought I'd ask - it might be useful to have a way of specifying regexes on strings.

I'm not sure how that would look or work though, some custom comment syntax or decorator?

Performance regression in 0.10

Input:

import { createIs } from 'typescript-is';

export const enum MyEnum {
  Alpha = 'alpha',
  Bravo = 'bravo',
  Charlie = 'charlie',
  Delta = 'delta',
  Echo = 'echo',
  Foxtrot = 'foxtrot',
  Golf = 'golf',
  Hotel = 'hotel',
  India = 'india',
  Juliet = 'juliet',
}
export const isMyEnum = createIs<MyEnum>();

typescript-is 0.9 output:

const isMyEnum = object => { return object === "alpha" || object === "bravo" || object === "charlie" || object === "delta" || object === "echo" || object === "foxtrot" || object === "golf" || object === "hotel" || object === "india" || object === "juliet"; };

export { isMyEnum };

typescript-is 0.10 output:

import { createIs } from 'typescript-is';

const isMyEnum = createIs(object => { var path = ["$"]; function _74(object) { if (object !== "alpha")
    return "validation failed at " + path.join(".") + ": expected string 'alpha'";
else
    return null; } function _76(object) { if (object !== "bravo")
    return "validation failed at " + path.join(".") + ": expected string 'bravo'";
else
    return null; } function _78(object) { if (object !== "charlie")
    return "validation failed at " + path.join(".") + ": expected string 'charlie'";
else
    return null; } function _80(object) { if (object !== "delta")
    return "validation failed at " + path.join(".") + ": expected string 'delta'";
else
    return null; } function _82(object) { if (object !== "echo")
    return "validation failed at " + path.join(".") + ": expected string 'echo'";
else
    return null; } function _84(object) { if (object !== "foxtrot")
    return "validation failed at " + path.join(".") + ": expected string 'foxtrot'";
else
    return null; } function _86(object) { if (object !== "golf")
    return "validation failed at " + path.join(".") + ": expected string 'golf'";
else
    return null; } function _88(object) { if (object !== "hotel")
    return "validation failed at " + path.join(".") + ": expected string 'hotel'";
else
    return null; } function _90(object) { if (object !== "india")
    return "validation failed at " + path.join(".") + ": expected string 'india'";
else
    return null; } function _92(object) { if (object !== "juliet")
    return "validation failed at " + path.join(".") + ": expected string 'juliet'";
else
    return null; } function _94(object) { var conditions = [_74, _76, _78, _80, _82, _84, _86, _88, _90, _92]; for (const condition of conditions) {
    var error = condition(object);
    if (!error)
        return null;
} return "validation failed at " + path.join(".") + ": there are no valid alternatives"; } var error = _94(object); return error; });

export { isMyEnum };

typescript-is 0.11 seems to have the same output as typescript-is 0.10

Especially given that the output of the isMyEnum function is just a boolean, this seems extremely wasteful in the 0.10+ versions.

TypeGuardError: validation failed at $: validation failed at $: superfluous property 'username' in object

interface HasPassword {
	password: string
}

interface HasUsername {
	username: string
}

type UsernamePassword = HasPassword & HasUsername

const object = {username: 'test', password: 'test'}

try {
	assertType<UsernamePassword>(object)
} catch (e) {
	console.log(e)
}

Result:

TypeGuardError: validation failed at $: validation failed at $: superfluous property 'username' in object
    at Object.assertType (/Users/LoganDark/nitrogen/backend/node_modules/typescript-is/index.js:58:15)
    at Object.<anonymous> (/Users/LoganDark/nitrogen/backend/index.ts:239:5)
    at step (/Users/LoganDark/nitrogen/backend/index.ts:32:23)
    at Object.next (/Users/LoganDark/nitrogen/backend/index.ts:13:53)
    at /Users/LoganDark/nitrogen/backend/index.ts:7:71
    at new Promise (<anonymous>)
    at __awaiter (/Users/LoganDark/nitrogen/backend/index.ts:3:12)
    at /Users/LoganDark/nitrogen/backend/index.ts:224:16
    at Layer.handle [as handle_request] (/Users/LoganDark/nitrogen/backend/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/LoganDark/nitrogen/backend/node_modules/express/lib/router/route.js:137:13) +1ms

Ignore the line numbers, I simplified the code for the issue.

Configuration:

{
	"transform": "typescript-is/lib/transform-inline/transformer",
	"ignoreClasses": true,
	"ignoreMethods": true,
	"disallowSuperfluousObjectProperties": true
}

The object does have username and password keys and they are both strings.

is/assertType functions throw error when interface has Date field

I have an interface which has a date field and when I try to assert an object against this interface the library throws the following error

Failed to transform node at: at transformNodeAndChildren (/xxx/node_modules/typescript-is/lib/transform-inline/transformer.js:27

interface MyInterface {
  id: string;
  createdAt: Date;
  modifiedAt: Date;
}

Does the library doesnt support interfaces with Date fields?

Problem with Date

interface IDevice {
  createdAt: Date;
}

const foo = { createdAt: new Date() } as any;
assertType<IDevice>(foo);

This causes:

Error: Encountered a method declaration, but methods are not supported. Issue: https://github.com/woutervh-/typescript-is/issues/5

Support TypeScript 3.7 Assertions

TypeScript 3.7 introduces the assert keyword microsoft/TypeScript#32695, it allows code to be written this way:

import { assertType } from 'typescript-is';

try {
    assertType<string>(object); // No need to reassign
    object.toUpperCasse();
} catch (error) {
    // ...
}

instead of this:

import { assertType } from 'typescript-is';

try {
    const asString = assertType<string>(object);
    asString.toUpperCasse();
} catch (error) {
    // ...
}

It'd be good if the library provided support for it, maybe using a new function if backwards compatibility is required.

Methods are not ignored in mapped types (maked with keyof syntax)

Since at the moment typescript-is does not support using is<T>() when T is a class, I have used a mapped type as this:

class MyClass {
    method() : string {
        return "";
    };
}

type MappedType = {
    [P in keyof MyClass]: MyClass[P];
}

but typescript-is does not check properly MappedType and throws a error, though the flag "ignoreMethods" is true: it not ignore the method, but expect the property contains an object:

let mappedType : MappedType = {
    method() {
        return "!!!";
    }
};

assertType<MappedType>(mappedType);

error log:

TypeGuardError: validation failed at $.method: expected an object
at Object.assertType (...\node_modules\typescript-is\index.js:62:15)
at Object. (...\my_test.js:10:21)
at Module._compile (internal/modules/cjs/loader.js:736:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:747:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at tryModuleLoad (internal/modules/cjs/loader.js:568:12)
at Function.Module._load (internal/modules/cjs/loader.js:560:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:801:12)
at executeUserCode (internal/bootstrap/node.js:526:15)
at startMainThreadExecution (internal/bootstrap/node.js:439:3)

Generic assertType results in "Unbound type parameter, missing type node."

This is a similar issue to #4. In my case I am getting the error when trying to use assertType<T>(data)

async function fetchContent<T>(filename: string): Promise<boolean> {
  const url = `/${filename}`;
  const res = await fetch(url);
  const data = await res.json();

  try {
    assertType<T>(data);
    return true;
  } catch (error) {
    console.log(error.message);
    return false;
  }
}

fetchContent<CMS.Homepage>('homepage.json');

This module should not be used in runtime. Instead, use a transformer during compilation.

I just wanted to drop this in here for your information. I don't know the root cause, or whether this issue even belongs in this repo but I thought it might be helpful for you to know about this.

When running with ts-node-dev --compiler ttypescript --respawn --transpileOnly src/index.ts, I get the error This module should not be used in runtime. Instead, use a transformer during compilation. when the validator runs. I don't get the error in parts of the program where the validator is not running.

Removing the --transpileOnly fixes the problem.

Fails to transform on empty tuples

Sample :

import { assertType } from 'typescript-is';

type Works = [number];
assertType<Works>([0]);

type Fails = [];
assertType<Fails>([]);

If you comment out the final line, all is good, if you don't you get (at compile time):

NestedError: Failed to transform node at: _test/ex.ts:6:17
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:33:15)
    at ts.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNode (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67586:23)
    at Object.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67860:59)
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
    at ts.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNodes (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67637:48)
    at visitLexicalEnvironment (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67670:22)
    at Object.visitEachChild (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript/lib/typescript.js:67974:54)
    at transformNodeAndChildren (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
Caused By: Error: Expected tuple type to have type arguments.
    at visitTupleObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-name.js:10:15)
    at visitObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-name.js:46:16)
    at Object.visitType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-name.js:114:16)
    at visitTupleObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:12:34)
    at visitObjectType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:161:16)
    at visitType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:347:16)
    at visitTypeReference (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:143:20)
    at Object.visitType (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:339:16)
    at createArrowFunction (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transform-node.js:16:36)
    at Object.transformNode (/Users/matthew.woolf/git/news-search/f-extract/node_modules/typescript-is/lib/transform-inline/transform-node.js:60:35)

Awesome project BTW.

`is` and `createIs` should error on excess properties

Is there a way to is or createIs that errors on excess properties?

interface IPerson {
   name: string;
}

const isPerson = createIs<IPerson>();

isPerson({name: "Arash", role: "Admin" }); // this should return false, but it returns true!

Use case: API server receives POST message with payload in the body, performs check to make sure payload conforms to expected schema, writes object to Cosmos DB (or other no-sql Document DB). Goal would be to 1. make sure payload has the properties that the interface expects, AND 2. payload does not have any other/extra properties not defined in the interface.

TypeScript itself errors on unexpected properties:

const person: IPerson = {name: "Arash", role: "Admin"}
// Type '{ name: string; role: string; }' is not assignable to type 'IPerson'.
// Object literal may only specify known properties, and 'role' does not exist in type 'IPerson'.ts(2322)

P.S. Big fan of typescript-is :)

Add option to work in transpiled source

For much faster development we use transpileOnly option and do typechecking in different background thread. Unfortunately this library fails with this option (see #14).

It would be great to add an option to skip type checking in such a case. Then assertType would never throw, is would always return true, etc.

We could achieve this by hacking typescript-is/index.js:

function assertType(obj, getErrorMessage = () => null) {
...

function is(obj, getErrorMessage = () => null) {
...

function createIs(getErrorMessage = () => null) {
...

function createAssertType(getErrorMessage = () => null) {
...

I hope you understand what do I mean :-)

Problem with optional properties

Hello!
I noticed some peculiarity when working with optional properties.
For example:

interface SomeInterface {
    someOptionalProperty?: string;
}

const value: SomeInterface = { someOptionalProperty: undefined };

This code is quite valid and can be compiled without any problems.

Example with assertType:
For example:

interface SomeInterface {
    someOptionalProperty?: string;
}

const value: SomeInterface = assertType<SomeInterface>({ someOptionalProperty: undefined });

In this case, I will get the error:

TypeGuardError: validation failed at $.someOptionalProperty: expected a string

Tuples are not type-checked correctly

ttypescript version: 3.2.4
typescript-is version: 0.7.18
node version: 11.6.0

Steps to reproduce:
Try compiling a file containing the following:

import { is } from 'typescript-is';

type NumTuple = [number, number];

console.log(is<NumTuple>([0]));
console.log(is<NumTuple>([]));

Actual results:
Both statements print true.

Expected results:
Both statements should print false, as neither [0] nor [] satisfy the NumTuple type.

I feel like I'm submitting too many bug reports :) Let me know if I can help any further. Also, if there's a way to donate to the project, I'd be happy to - this is one of the most useful libraries I've used in a long time!

[Request] add a changelog

Hi! I saw that a new version was published just a few days ago, though its hard to say that that release actually changed (though I did my best to dig through the commits). It would be awesome if you started keeping a CHANGELOG.txt or used github releases for future versions so we can see what new features/fixes/breaking changes are part of that release.

Of course, its more work to maintain, but it helps you (the maintainer) look back on what you have done over time too.

This may help, this may not, but on one of my personal projects (of which I am using typescript-is ๐ŸŽˆ!) I automate the process of publishing so that travis ci runs npm publish for me upon creating a github release https://github.com/andykais/scrape-pages/blob/319dd19b0c952bd9b72ba4f23b3995c25fc6bd81/.travis.yml

"Unsupported declaration kind" when validating against a class containing a method

ttypescript version: 3.2.4
typescript-is version: 0.7.16
node version: 11.6.0

Steps to reproduce:
Try compiling a file containing the following:

import { is } from 'typescript-is';

class X {
    method() {}
}
console.log(is<X>(new X()));

Actual results:
The compilation fails with an error like this:

typescript-is: transforming program with 23 source files; using TypeScript 3.2.4.

undefined:86995
                throw e;
                ^
NestedError: Failed to transform node at: /home/geomaster/Projects/tests/typescript-is/index.ts:6:13
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:27:15)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
    at visitNodes (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63441:48)
    at Object.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63610:156)
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
    at visitNode (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63390:23)
    at Object.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63664:59)
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
Caused By: Error: Unsupported declaration kind: 156
    at visitDeclaration (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:52:15)
    at visitPropertySymbol (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:77:25)
    at visitRegularObjectType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:131:29)
    at visitObjectType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:172:16)
    at Object.visitType (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/visitor.js:366:16)
    at createArrowFunction (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transform-node.js:9:21)
    at Object.transformNode (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transform-node.js:58:35)
    at transformNodeAndChildren (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:22:44)
    at ts.visitEachChild (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
    at visitNodes (/home/geomaster/Projects/tests/typescript-is/node_modules/typescript/lib/typescript.js:63441:48)

Expected results:
I know that class validation is explicitly not supported, which is completely fine. But this only breaks when the class has a method, which is a bit hard to track down and confusing. It might be more intuitive to display a normal compile error as soon as a class is tested.

This is not a high-priority issue, but might be good to put it on the backlog - I had quite some trouble trying to figure out what exactly is happening here :)

document how to use typescript-is without ttypescript

In my project I use webpack and I am able to use typescript-is without the ttypescript dependency. ts-loader has the ability to pass transformers directly into it. The following is enough to get typescript-is validators up and running.

const typescriptIsTransformer = require('typescript-is/lib/transform-inline/transformer').default

module.exports = {
  // I am hiding the rest of the webpack config
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        options: {
          getCustomTransformers: program => ({
            before: [typescriptIsTransformer(program)]
          })
        }
      },

}

Just a thought if you want to drop it into your readme

Doesnโ€™t Seem to Detect Additional Keys

Either Iโ€™m doing something wrong, or this is not yet supported:

interface Example {
    exists: true
}

const breaks = { exists: true, doesNotExist: true }

is<Example>(breaks)
// expected: false, received: true

Feature request: support .enforce<SomeType>() syntax

Hi.

Currently, to wrap some async request call with the library, one has to write:

const res = assertType<{
  name: string;
  age?: number;
}>(await client.request(
  โ€œhttps://example.comโ€,
  { some_post_data: 123, other: โ€œabcโ€ }
));

It doesnโ€™t look nice:

  1. Too many nested parens.
  2. The response shape goes before the request shape which is counter-intuitive.

A way better syntax would be:

const res = await client
  .request(
    โ€œhttps://example.comโ€,
    { some_post_data: 123, other: โ€œabcโ€ }
  )
  .enforce<{
    name: string;
    age?: number;
  }>();

(This is how Prettier would format it.)

Is it possible to implement this .postfix-like syntax in the transformer?

Notice 3 things here:

  1. The whole purpose is to let it work with promises. Without working with promises such syntax is pretty useless since it produces nested parens again.
  2. Itโ€™s okay for request() to return something custom (in terms of typing information), like a promise with additional enforce() method in it (along with .then and .catch - Promise<T> & EnforcePromiseMixin<T>)), itโ€™s easily expressible in TS I guess.
  3. If the validation fails, itโ€™s critical to have a detailed error message with all the info about mismatching properties.

If itโ€™s possible to implement such a thing (is it possible in theory at all?), it would be a huge relief IMHO. There is no other library which supports such things.

Usage without ttypescript?

Is there a way to manually run a transform and import the transformed code for this library? I'm using create-react-app and don't want to eject in order to change my webpack loaders

is<Isomething>() -> NestedError: Failed to transform node

attach.zip

kai@desktop:/bigdata/KAI/projects/Mechanic$ ./prepare-checkin.sh
yarn run v1.13.0
$ ttsc
typescript-is: transforming program with 98 source files; using TypeScript 3.2.4.

/bigdata/KAI/projects/Mechanic/node_modules/typescript/lib/typescript.js:86995
throw e;
^
NestedError: Failed to transform node at: /bigdata/KAI/projects/Mechanic/src/io/readRuleFile.ts:28:19
at transformNodeAndChildren (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:27:15)
at ts.visitEachChild (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
at visitNode (/bigdata/KAI/projects/Mechanic/node_modules/typescript/lib/typescript.js:63390:23)
at Object.visitEachChild (/bigdata/KAI/projects/Mechanic/node_modules/typescript/lib/typescript.js:63694:154)
at transformNodeAndChildren (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
at ts.visitEachChild (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
at visitNodes (/bigdata/KAI/projects/Mechanic/node_modules/typescript/lib/typescript.js:63441:48)
at Object.visitEachChild (/bigdata/KAI/projects/Mechanic/node_modules/typescript/lib/typescript.js:63696:63)
at transformNodeAndChildren (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:29:15)
at ts.visitEachChild (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/transformer.js:29:62)
Caused By: Error: Unsupported declaration kind: 155
at visitDeclaration (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:52:15)
at visitPropertySymbol (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:77:25)
at visitRegularObjectType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:131:29)
at visitObjectType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:172:16)
at visitType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:371:20)
at type.types.map (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:213:24)
at Array.map ()
at visitUnionOrIntersectionType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:213:10)
at visitType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:380:16)
at visitArrayObjectType (/bigdata/KAI/projects/Mechanic/node_modules/typescript-is/lib/transform-inline/visitor.js:66:33)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Feature: function argument specific error messages

This may be getting into the territory of custom error reporting (io-ts#error-reporting for example). But it could also just be a special assertion generator. The idea is that I would like to validate a function's parameters. typescript-is provides all the validation I need to make that happen, but the errors it gives back are not as helpful to an end user. This is easier shown than explained:

type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never
type Fn = (a: number, b: string) => boolean
type Args = ArgumentTypes<Fn>

const validateFn = createAssertType<ArgumentTypes<Fn>>()
const fn: Fn = (...args) => {
    validateFn(args)
    const [a, b] = args
    return true
}


fn() // TypeGuardError: validation failed at $: expected an array of length 2
// could be: TypeGuardError: validation failed at $: expected 2 arguments

fn(1, 2) // TypeGuardError: validation failed at $.[1]: expected a string
// could be TypeGuardError: validation failed at argument 2: expected a string

fn(1, 'b') // correct usage

I think a wrapAssertFunctionParams<T> would be handy, as a higher order function. Usage would look like:

// generic type argument could be optional
const fnWithValidation = wrapAssertFunctionParams<Fn>(fn)

For the time being I will be writing something myself for my library which does just this. I can post the implementation here if it is interesting

ts-auto-guard collab?

Hey @woutervh-. Looks like we're both working on similar tools. I would have emailed you privately but I can't find your contact details.

Maybe it would make sense to work together instead of building the two tools in parallel?

I've got a debug mode that will explain why the check failed. You've got some of my planned features (assertions etc). Generally speaking I think I prefer your API and your project seems more mature than ts-auto-guard. Without having looked I assume you've got a nicer code base too. ;)

Let me know what you think.

Ignore Methods not Supported or Support RegExp Validation

I just tried to validate that one of my optional config variables is a RegExp. typescript-is complained and told me:

Error: Encountered a method declaration, but methods are not supported. Please check the README

Would it be possible to either support RegExp, or to opt-out of this error explicitly in the is<...>(...)?

Obtaining type info as JSON at runtime

Hi,

I have the following use case, and it seems your project does something pretty close, so was wondering if you might want to add it.

I've been working with the helpful CLI argument processing utility command-line-args, but found it redundant to have to define what is basically a schema of definitions and then represent that same schema all over again in my jsdoc (or TypeScript) annotations.

I'd like to be able to define interfaces in TS or jsdoc, and then at runtime, pass a JSON form of these interfaces onto a utility function.

So in place of this (I don't know if your code works with jsdoc typedef's, so if not, just imagine the typedefs are interfaces or types instead):

/**
 * @typedef {Object} MyCLIOptions
 * @property {boolean} v verbose
 * @property {string[]} src [default]
 * @property {number} t timeout
 */
const optionDefinitions = [
  { name: 'verbose', alias: 'v', type: Boolean },
  { name: 'src', type: String, multiple: true, defaultOption: true },
  { name: 'timeout', alias: 't', type: Number }
]

I'd like to be able to do something like this (I know it might not build the exact same JSON as the above array (and as a generic tool it might not parse the [default] text but could hopefully at least pass on the jsdoc names/descriptions), but if it could be parsed by a utility, that'd be enough):

import {typeAsJSON} from 'typescript-is';
/**
 * @typedef {Object} MyCLIOptions
 * @property {boolean} v verbose
 * @property {string[]} src [default]
 * @property {number} t timeout
 */
const optionDefinitions = typeAsJSON('MyCLIOptions');

// I think I prefer the line above for being valid JS (not even needing TS), but
// possibly these if necessary?

// const optionDefinitions = typeAsJSON(MyCLIOptions);
// Or this?
// const optionDefinitions : MyCLIOptions = typeAsJSON();

If this is beyond your project's scope, do you have any suggestions for a novice to TypeScript?

Optional property set to undefined fails validation

interface Test {
  a?: string
}
assertType<Test>({ a: undefined })
// TypeGuardError: validation failed at $.a: expected a string

Is this expected behavior? I thought a?: string and a: string | undefined were equivalent in TypeScript.

Incorrect behavior for nullable properties with strictNullChecks disabled

When strictNullChecks is set to false, nullable properties are not allowed to be null. I suspect this is related to the TypeScript compiler transforming string | null to string in this mode. A potential fix could be allowing null/undefined for all types within typescript-is if it detects strictNullChecks are disabled?

import { createIs } from 'typescript-is'

interface Foo {
  nullableString: string | null
}

const isFoo = createIs<Foo>()

const validFoo: Foo = {
  nullableString: null,
}

// should be true, but returns false!
console.log(isFoo(validFoo))

Bug: Does not support recursive types

Recursive types do not compile, and throw a RangeError: Maximum call stack size exceeded. Let me know if this is a problem with ttypescript and I can report this bug upstream. Thank you for working on this library! ๐Ÿ˜„

stack trace:

$ npm run compile:runtime:config

/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:87527
                throw e;
                ^
NestedError: Failed to transform node at: /Users/andrew/Code/scratchwork/scrape-pages/src/settings/config/assert.ts:9:55
    at transformNodeAndChildren (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:30:15)
    at ts.visitEachChild (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:32:62)
    at visitNode (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:63887:23)
    at Object.visitEachChild (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:64161:59)
    at transformNodeAndChildren (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:32:15)
    at ts.visitEachChild (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:32:62)
    at visitNodes (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:63938:48)
    at Object.visitEachChild (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:64157:45)
    at transformNodeAndChildren (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:32:15)
    at ts.visitEachChild (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/transformer.js:32:62)
Caused By: RangeError: Maximum call stack size exceeded
    at Object.isNodeKind (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:13880:24)
    at Object.createNode (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:16206:22)
    at createSynthesizedNode (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:59514:23)
    at Object.createTypeOf (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript/lib/typescript.js:60561:20)
    at visitString (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:470:131)
    at visitType (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:514:16)
    at createPropertyCheck (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:12:24)
    at visitPropertySignature (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:51:12)
    at visitDeclaration (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:55:16)
    at visitPropertySymbol (/Users/andrew/Code/scratchwork/scrape-pages/node_modules/typescript-is/lib/transform-inline/visitor.js:111:16)

simplified failing snippet:

import { assertType } from 'typescript-is'
type ConfigInit = {
  folder: string
  children: ConfigInit
}

export const assertConfigType = (configInit: any) => {
  assertType<ConfigInit>(configInit)
}

tsconfig.json:

{
  "compilerOptions": {
    "rootDir": "../../../../src",
    "outDir": "./",
    "sourceMap": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "lib": ["es2015", "es2016", "es2017"],
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "es2017",
    "allowJs": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "plugins": [
      {
        "transform": "typescript-is/lib/transform-inline/transformer"
      }
    ]
  },
  "include": ["src/**/*.ts", "custom.d.ts"],
  "files": ["../assert.ts", "../../../../custom.d.ts"]
}

Using types in interfaces/complex types throws a Class error

I'm trying to use typescript-is to provide runtime validation for a library that manipulates a custom AST that is serialized/deserialized from well structured JSON. I've written out the type definition using only interfaces and a few basic types. When trying to compile it throws the following error

NestedError: Failed to transform node at: /Users/tristan/code/mobiledoc-core/lib/index.ts:53:32
    at transformNodeAndChildren (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:33:15)
    at ts.visitEachChild (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNode (/Users/tristan/code/mobiledoc-core/node_modules/typescript/lib/typescript.js:60504:23)
    at Object.visitEachChild (/Users/tristan/code/mobiledoc-core/node_modules/typescript/lib/typescript.js:60778:59)
    at transformNodeAndChildren (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
    at ts.visitEachChild (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
    at visitNodes (/Users/tristan/code/mobiledoc-core/node_modules/typescript/lib/typescript.js:60555:48)
    at Object.visitEachChild (/Users/tristan/code/mobiledoc-core/node_modules/typescript/lib/typescript.js:60774:45)
    at transformNodeAndChildren (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:35:15)
    at ts.visitEachChild (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/transformer.js:35:62)
Caused By: Error: Classes cannot be validated. https://github.com/woutervh-/typescript-is/issues/3
    at Object.checkIsClass (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-utils.js:23:19)
    at visitObjectType (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:155:22)
    at visitType (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:346:16)
    at visitTypeReference (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:142:20)
    at visitType (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:338:16)
    at visitTypeReference (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:142:20)
    at visitType (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:338:16)
    at ts.createFunctionDeclaration.ts.createBlock.propertyInfos.map (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:100:23)
    at Array.map (<anonymous>)
    at VisitorUtils.setFunctionIfNotExists (/Users/tristan/code/mobiledoc-core/node_modules/typescript-is/lib/transform-inline/visitor-type-check.js:94:30)

Here are the type definitions

// Cards
type CardPayload = Record<string, any>
type Card = [string, CardPayload]

// Atoms
type Atom = [string, string, unknown]

// Markers
// [typeIdentifier, openMarkupsIndexes, numberOfClosedMarkups, text]
type TextMarker = [0, number[], number, string]
// [typeIdentifier, openMarkupsIndexes, numberOfClosedMarkups, atomIndex]
type AtomMarker = [1, number[], number, number]
type Marker = TextMarker | AtomMarker

// Sections Tags enums / unions
type MarkupSectionTag = 'aside' | 'blockquote' | 'pull-quote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'
type ListSectionTag = 'ul' | 'ol'

// Section Identifiers
type MarkupIdentifer = 1
type ImageIdentifer = 2
type ListIdentifer = 3
type CardIdentifer = 10

// Sections
type MarkupSection = [MarkupIdentifer, MarkupSectionTag, Marker[]]
type ImageSection = [ImageIdentifer, string]
type ListSection = [ListIdentifer, ListSectionTag, Marker[][]]
type CardSection = [CardIdentifer, number]
type Section = MarkupSection | ImageSection | ListSection | CardSection

// Markups
type MarkupTag = 'a' | 'b' | 'code' | 'em' | 'i' | 's' | 'strong' | 'sub' | 'sup' | 'u'

type Markup = [MarkupTag, string[] | undefined]

// Mobiledoc Interface
export interface Mobiledoc_0_3_2 {
  atoms: Atom[]
  cards: Card[]
  markups: Markup[]
  sections: Section[]
  readonly version: '0.3.2'
}

is<Mobiledoc_0_3_2>(data) //=> throws compilation error
is<Section>(data) //=> throws compilation error
is<Markup>(data) //=> throws compilation error
is<Card>(data) //=> ok
is<Atom>(data) //=> ok

The interesting thing is that if I make the Mobiledoc_0_3_2 only have sections and cards properties it still throws an error, however if I make them not an array of those types it seems to work fine. Perhaps Array is being seen as a class?

Validation Context: Which property is not valid on an object

Is it possible to determine which property is valid on an object from the AssertType function? Currently I only see Type assertion failed

Something like the following would be helpful:

interface MyRequest {
  name: string,
  age: number
}

try {
  const asString = assertType<MyRequest>(unknownObj);
} catch (e) {
  console.log('error: ', e); // "error: Object does not have property: 'name'   "
}

Incorrect transform for optional parameters

The length check for a Parameter list tuple is too strict in the case of optional parameters (or in fact any array where the final element(s) can validly be undefined).

Here's a case to reproduce the issue:

import * as assert from 'assert';
import { createIs } from '../index';

describe('is', () => {
    function foo(_a: number, _b?: string) { return; }
    type FooParams = Parameters<typeof foo>;

    const isFooParams = createIs<FooParams>();

    describe('isFooParams', () => {
        it('should return true for valid parameters, optional or not', () => {
            assert.deepStrictEqual(isFooParams([1, 'a']), true);
            assert.deepStrictEqual(isFooParams([1]), true);
            assert.deepStrictEqual(isFooParams([1, undefined]), true);
            assert.deepStrictEqual(isFooParams([1, null]), false);
        });
    });
});

The second assert (incorrectly) fails: element [1] of the array is indeed undefined.

In the generated output (below) there is an inappropriate test for Array.length===2.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
const index_1 = require("../index");
describe('is', () => {
    function foo(_a, _b) { return; }
    const isFooParams = index_1.createIs(object => {
        var path = ["$"]; 
        function _number(object) {
            if (typeof object !== "number")
                return "validation failed at " + path.join(".") + ": expected a number";
            else
                return null;
        } 
        function _undefined(object) {
            if (object !== undefined)
                return "validation failed at " + path.join(".") + ": expected undefined";
            else
                return null;
        } 
        function _string(object) {
            if (typeof object !== "string")
                return "validation failed at " + path.join(".") + ": expected a string";
            else
                return null;
        } 
        function su__undefined__string_eu(object) {
            var conditions = [_undefined, _string]; for (const condition of conditions) {
                var error = condition(object);
                if (!error)
                    return null;
            } return "validation failed at " + path.join(".") + ": there are no valid alternatives";
        } 
        function st__number_su__undefined__string_eu_et_9_396(object) {
/* HERE: array.length might be <= 2 since all the final elements after length=1 can validly be undefined */
            if (!Array.isArray(object) || object.length !== 2) 
                return "validation failed at " + path.join(".") + ": expected an array of length 2"; {
                path.push("[0]");
                var error = _number(object[0]);
                path.pop();
                if (error)
                    return error;
            } {
                path.push("[1]");
                var error = su__undefined__string_eu(object[1]);
                path.pop();
                if (error)
                    return error;
            } return null;
        } var error = st__number_su__undefined__string_eu_et_9_396(object); return error;
    });
    describe('isFooParams', () => {
        it('should return true for valid parameters, optional or not', () => {
            assert.deepStrictEqual(isFooParams([1, 'a']), true);
            assert.deepStrictEqual(isFooParams([1]), true);
            assert.deepStrictEqual(isFooParams([1, undefined]), true);
            assert.deepStrictEqual(isFooParams([1, null]), false);
        });
    });
});

Output

Accessing the source data in TypeGuardError

TypeGuardError forms a nice error message, but the data which failed validation is not part of this error. I.e. I can catch & print "TypeGuardError: validation failed at ...", but I can't print "TypeGuardError: validation failed at ..., input data was: ..." down the stack.

Is it possible to add a field to TypeGuardError which holds the actual data failed validation? This would be super useful for debugging.

[Feature Request] babel macro plugin

Hi! This one is a big ask, but I want to know if it is in the works at all. Currently, this is what my webpack loaders setup looks like:

  module: {
    rules: [
      {
        test: /\.(ts|js)$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      },
      {
        test: /\.runtime\.ts/,
        exclude: /node_modules/,
        use: [{ loader: 'ts-loader', options: { compiler: 'ttypescript' } }]
      },
    ]
  },

and my .babelrc

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "node": "8.0"
        }
      }
    ],
    "@babel/typescript"
  ],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

Before introducing typescript-is, I used the single babel-loader to transpile both typescript and javascript. Now, the code has to go through two different loaders, which slows the process significantly, since typechecking the entire project is essential for the loader to properly transform the code. It also adds more dependencies specifically for this library, and makes tsconfig.json non-standard.

A similar framework to the typescript transformer api in babel land is the babel-plugin-macros project. Importing a typescript-is.macro would operate identically to the current import/transform.

Of course, this hinges on the hope that a babel macro and a typescript transformer could share some core code. If not, then maintaining two alongside each other is likely too much of an undertaking.

Allow at least typeof/instanceof checks for basic methods/functions

I know you mentioned before that this strives to only work for serializable data.

However, ๐Ÿ˜ would it be possible, happily hidden behind an options flag, to not simply ignore methods/functions but to allow at least a simple type check of sorts, to at least make the minimal guarantee that the provided value is actually a function and not e.g. a number?

Transformer Performance

I was just trying out your transformer as I planned to write something similar myself until I found yours, but on a medium sized library just enabling the transformer I see a 21% increase in compile time (from 3.82 seconds to 4.63 on a 16 core machine with all NVMe storage).

Is that expected?

Using ttypescript (ttsc)

Possibility to use with react-scripts

Hello!

I use react-scripts to set up my react project. It would be nice to use typescript-is with it without "ejecting". For the first trial I have added the plugins section in my tsconfig.json.

Unfortunately, I've got this error:

Error: This module should not be used in runtime. Instead, use a transformer during compilation.

Do you have any information what to do when I want to use it with react-scripts?

Usage in linked modules

When developing a module we often npm link it to a parent application for testing. This way we temporarily rely on the parent app's compiler - which has neither ttypescript, nor it checks the linked module's tsconfig file -> meaning it will not run the transformer and is<...> will not be generated. This way it's pretty much impossible to develop using npm link unless I'm missing something.

Do you have an idea for this situation?

Idea: Emit is.ts, bundling validators and assertions for all discovered types and interfaces

First: Thanks for this fantastic work! Finally bringing runtime sanity to objects, APIs, files, etc.

So I'm in the process of adding validators across my code; something like:

interface Person {
   name: string;
   age: number;
}

const isPerson = createIs<Person>();

which means that I'm repeating that isInterface = createIs<Interface>() line for all my interfaces and types.

Would you consider automatically generating is[Interface], assert[Interface] and other checker and assertion functions and emitting them in an is.ts (or is.js) file, usable for runtime?

Same idea as how TypeScript can produce declarations, your library can produce a validator/checker/assertion bundle.

ttsc --watch does not emit type validators

Having the following code:

createAssertType<{
  data: {
    node: { a: string; closed: string; b: string };
  };
}>()({ data: { node: { a: "a", closed: false, b: "b" } } });

When running yarn ttsc --watch and making changes in this file, here is the compiled result:

image

But if I run yarn ttsc, then the compiled result becomes correct:

image

Why not check classes?

"This library will not check classes. Instead, you are encouraged to use the native instanceof operator."

Wouldn't it be easy to have this library generate the instanceof code? What's the reason not to?

[Feature Request] use custom error when throwing assertions

It would be nice if the error thrown from assertType and createAssertType used a custom error and exposed that error from the library.

Currently my code using assertions looks like this:

// assert.ts
const assertConfigType = (config: any) => {
  try {
    assertType<Config>(config)
  } catch (e) {
    throw new RuntimeTypeError(e.message)
  }
}
// further up the stack
try {
  ...
} catch (e) {
  if (e instanceof RuntimeTypeError) {
    console.log('poorly formed config given')
    console.log(e.message)
    process.exit(1)
  }
}

It would be nice if I could simplify the code to this:

// assert.ts
const assertConfigType = (config: any) => assertType<Config>(config)

// further up the stack
import { RuntimeTypeError } from 'typescript-is'
try {
  ...
} catch (e) {
    if (e instanceof RuntimeTypeError) {
      ...
    }
}

For reference, node-fetch does this with the FetchError class https://www.npmjs.com/package/node-fetch#class-fetcherror

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.