Coder Social home page Coder Social logo

janjakubnanista / ts-type-checked Goto Github PK

View Code? Open in Web Editor NEW
84.0 1.0 5.0 3.83 MB

Runtime duck type checking utilities for TypeScript.

License: MIT License

JavaScript 3.23% Shell 3.26% TypeScript 92.93% Makefile 0.57%
guards typescript duck-typing typing type-guards type-safety

ts-type-checked's Introduction

ts-type-checked

Automatic type guards for TypeScript

GitHub build status NPM Version Dev Dependency Status Known Vulnerabilities License

ts-type-checked generates type guards based on your own (or library) TypeScript types. It is compatible with Rollup, Webpack and ttypescript projects and works nicely with Jest, Mocha or ts-node

Example cases | Installation | API | Supported types

Wait what?

As they say an example is worth a thousand API docs so why not start with one.

interface WelcomeMessage {
  name: string;
  hobbies: string[];
}

//
// You can now turn this
//
const isWelcomeMessage = (value: any): message is WelcomeMessage =>
  !!value &&
  typeof value.name === 'string' &&
  Array.isArray(value.hobbies) &&
  value.hobbies.every(hobby => typeof hobby === 'string');

//
// Into this
//
const isWelcomeMessage = typeCheckFor<WelcomeMessage>();

//
// Or without creating a function
//
if (isA<WelcomeMessage>(value)) {
  // value is a WelcomeMessage!
}

Motivation

TypeScript is a powerful way of enhancing your application code at compile time but, unfortunately, provides no runtime type guards out of the box - you need to create these manually. For types like string or boolean this is easy, you can just use the typeof operator. It becomes more difficult for interface types, arrays, enums etc.

And that is where ts-type-checked comes in! It automatically creates these type guards at compile time for you.

This might get useful when:

  • You want to make sure an object you received from an API matches the expected type
  • You are exposing your code as a library and want to prevent users from passing in invalid arguments
  • You want to check whether a variable implements one of more possible interfaces (e.g. LoggedInUser, GuestUser)
  • ...

Example cases

Checking external data

Imagine your API spec promises to respond with objects like these:

interface WelcomeMessage {
  name: string;
  greeting: string;
}

interface GoodbyeMessage {
  sayByeTo: string[];
}

Somewhere in your code there probably is a function just like handleResponse below:

function handleResponse(data: string): string {
  const message = JSON.parse(data);

  if (isWelcomeMessage(message)) {
    return 'Good day dear ' + message.name!
  }

  if (isGoodbyeMessage(message)) {
    return 'I will say bye to ' + message.sayByeTo.join(', ');
  }

  throw new Error('I have no idea what you mean');
}

If you now need to find out whether you received a valid response, you end up defining helper functions like isWelcomeMessage and isGoodbyeMessage below.

const isWelcomeMessage = (value: any): value is WelcomeMessage =>
  !!value &&
  typeof value.name === 'string' &&
  typeof value.greeting === 'string';

const isGoodbyeMessage = (value: any): value is GoodbyeMessage =>
  !!value &&
  Array.isArray(value.sayByeTo) &&
  value.sayByeTo.every(name => typeof name === 'string');

Annoying isn't it? Not only you need to define the guards yourself, you also need to make sure the types and the type guards don't drift apart as the code evolves. Let's try using ts-type-checked:

import { isA, typeCheckFor } from 'ts-type-checked';

// You can use typeCheckFor type guard factory
const isWelcomeMessage = typeCheckFor<WelcomeMessage>();
const isGoodbyeMessage = typeCheckFor<GoodbyeMessage>();

// Or use isA generic type guard directly in your code
if (isA<WelcomeMessage>(message)) {
  // ...
}

Type guard factories

ts-type-checked exports typeCheckFor type guard factory. This is more or less a syntactic sugar that saves you couple of keystrokes. It is useful when you want to store the type guard into a variable or pass it as a parameter:

import { typeCheckFor } from 'ts-type-checked';

interface Config {
  version: string;
}

const isConfig = typeCheckFor<Config>();
const isString = typeCheckFor<string>();

function handleArray(array: unknown[]) {
  const strings = array.filter(isString);
  const configs = array.filter(isConfig);
}

// Without typeCheckFor you'd need to write
const isConfig = (value: unknown): value is Config => isA<Config>(value);
const isString = (value: unknown): value is Config => isA<string>(value);

Reducing the size of generated code

isA and typeCheckFor will both transform the code on per-file basis - in other terms a type guard function will be created in every file where either of these is used. To prevent duplication of generated code I recommend placing the type guards in a separate file and importing them when necessary:

// in file typeGuards.ts
import { typeCheckFor } from 'ts-type-checked';

export const isDate = typeCheckFor<Date>();
export const isStringRecord = typeCheckFor<Record<string, string>>();

// in file myUtility.ts
import { isDate } from './typeGuards';

if (isDate(value)) {
  // ...
}

Useful links

  • ts-trasformer-keys, TypeScript transformer that gives you access to interface properties
  • ts-auto-mock, TypeScript transformer that generates mock data objects based on your types

ts-type-checked's People

Contributors

dependabot[bot] avatar eliasgroll avatar feikerwu avatar janjakubnanista 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

Watchers

 avatar

ts-type-checked's Issues

Babel intergration

I came across this library while looking for a means of runtime type assertion. It seems like a very good program. Is it possible to use this with Babel too?

"strict": true not respected

Given some code

import { isA } from "ts-type-checked";

console.log(isA<string>(null));
console.log(isA<string>(undefined));

true is outputted. This should be false, especially if strict: true is set in the tsconfig.json.

Optional boolean causes error

Hey there!

So my issue is the following:

This type:

type FieldOptions = {
   label?: string
   description?: string
   repeat?: boolean
}

when used in the isA function throws the following error

    !isA<FieldOptions>(options) // ...
/Users/loic/Documents/xxx/projects/yyy/node_modules/typescript/lib/typescript.js:110417
                throw e;
                ^

Error: Could not find value for a literal type false

However neither does this type

type FieldOptions = {
   label?: string
   description?: string
   repeat: boolean
}

nor this one

type FieldOptions = {
   label?: string
   description?: string
   repeat?: number
}

throw the same error. The last two types just work and compile fine.

Is there any way around that? Or any reason for why this is happening, so I can take a dive into the code myself?

Thanks in regards!

Does not work with Nest.JS webpack compiler

Here is my webpack.config.js:

const transformer = require('ts-type-checked/transformer');

module.exports = function (options) {
    return {
        ...options,
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    loader: 'ts-loader',
                    options: {
                        getCustomTransformers: (program) => ({
                            before: [transformer(program)]
                        })
                    }
                }
            ]
        }
    };
};

After many deprecated warnings, webpack build failed:


> [email protected] start:dev
> nest start --watch


 Info  Webpack is building your sources...

DeprecationWarning: 'createIdentifier' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'updateSourceFile' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createLiteral' has been deprecated since v4.0.0. Use `factory.createStringLiteral`, `factory.createStringLiteralFromNode`, `factory.createNumericLiteral`, `factory.createBigIntLiteral`, `factory.createTrue`, `factory.createFalse`, or the factory supplied by your transformation context instead.
DeprecationWarning: 'createParameterDeclaration' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createToken' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: '' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createCallExpression' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createStrictInequality' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createNull' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createParenthesizedExpression' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createLogicalAnd' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createElementAccessExpression' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createTypeOfExpression' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createStrictEquality' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createLogicalOr' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createArrowFunction' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createVariableDeclaration' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createVariableStatement' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createPropertyAssignment' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
DeprecationWarning: 'createObjectLiteralExpression' has been deprecated since v4.0.0. Use the appropriate method on 'ts.factory' or the 'factory' supplied by your transformation context instead.
C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Compilation.js:4667
                                                                if (!inTry) throw err;
                                                                            ^

TypeError: Cannot read properties of null (reading 'toString')
    at Watching.afterCallback [as handler] (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\@nestjs\cli\lib\compiler\webpack-compiler.js:60:39) 
    at handleError (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Watching.js:246:9)
    at Watching._done (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Watching.js:282:19)
    at onCompiled (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Watching.js:171:27)
    at C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Compiler.js:1192:25
    at finalCallback (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Compilation.js:2790:11)
    at C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Compilation.js:3062:18
    at eval (eval at create (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:12:1)
    at fn (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\webpack\lib\Compilation.js:481:17)
    at Hook.eval [as callAsync] (eval at create (C:\Users\Henry\Documents\xxxxxx\xxxxxx\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:10:1)

Node.js v18.13.0

Add support for generic classes

At the moment generic classes are not supported (since instanceof only is used to check for a particular class).

Important the existing support for non-generic classes, especially Date should not be broken (#1)

Does not work for object keys which contain dashes.

Thank you for making this library!

I create a GRPC-like JSON-RPC library based on WebSockets and it becomes super useful when we want to check the type of incoming messages.

We often have the case that incoming messages (especially http request headers) include dashes, and noticed this:

Expected behavior

A snippet like:

type Test = {"test-dash":number};

const check = (test: Test) => isA<Test>(test);

should not throw - i.o.w compile to:

var __typeCheckerMap__ = {
    __0: function (value) { return typeof value === "object" && value !== null && __typeCheckerMap__.__1(value.['test-dash']); },
    __1: function (value) { return typeof value === "number"; }
};
const check = (test) => __typeCheckerMap__.__0(test);

Current behavior

It throws an exception because it compiles to (because of the dash):

var __typeCheckerMap__ = {
    __0: function (value) { return typeof value === "object" && value !== null && __typeCheckerMap__.__1(value.test-dash); },
    __1: function (value) { return typeof value === "number"; }
};
const check = (test) => __typeCheckerMap__.__0(test);

Possible solution

Maybe change createElementAccess to use ts.createPropertyAccess ? Not super sure because it was hard to find any docs on that.

type checks on 0.5.0 seems to be broken

I found an issue when you switch from v0.4.2 to v0.5.0. With the same code and same config, it worked correctly in 0.4.2 but broke completely in 0.5.0, I couldn't test 0.6.0 because it isn't on npm ?

Example Code:

import { isA } from 'ts-type-checked';

type MyTest = {
    name: string;
    pos: number;
}

const objToCheck = {
    name: 'test',
    pos: 2
}; 

if (isA<MyTest>(objToCheck)) {
    console.log('objToCheck is MyTest');
}


if (isA<MyTest>({})) {
    console.log('empt object is MyTest');
}

tsconfig:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "esModuleInterop": true,
    "plugins": [
      {
        "transform": "ts-type-checked/transformer"
      }
    ]
  },
  "files": [
    "test.ts"
  ]
}

Note: same results if you have strict: true in your tsconfig

generates with 0.4.2:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ___isA___ = { "0": function (value) { return ((value !== undefined) && (value !== null)) && (typeof value["name"] === "string") && (typeof value["pos"] === "number"); } };
var objToCheck = {
    name: 'test',
    pos: 2
};
if (___isA___["0"](objToCheck)) {
    console.log('objToCheck is MyTest');   // <<< is printed
}
if (___isA___["0"]({})) {
    console.log('empt object is MyTest');
}

with 0.5.0:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ___isA___ = { "0": function (value) { return (value === null) || (value === undefined) || (((value !== undefined) && (value !== null)) && ((value["name"] === null) || (value["name"] === undefined) || (typeof value["name"] === "string")) && ((value["pos"] === null) || (value["pos"] === undefined) || (typeof value["pos"] === "number"))); } };
var objToCheck = {
    name: 'test',
    pos: 2
};
if (___isA___["0"](objToCheck)) {
    console.log('objToCheck is MyTest');     // <<< is printed
}
if (___isA___["0"]({})) {
    console.log('empt object is MyTest');    // <<< is also printed
}

packages:

        "ts-type-checked": "0.5.0",   // or 0.4.2
        "ttypescript": "^1.5.12",
        "typescript": "3.9.5",
        "tslib": "2.0.1"

Add support for Date

Date is now currently not supported since it does not resolve to a class. This can be solved by forcing resolution of an interface type (possibly checking for some key properties).

Important since people can override Date interface this needs to be configurable via an option.

Does not enforce required attributes

HI. Thanks for such a wonderful tool! I'm loving it. I'm using it to validate my YML settings file.
I see that the transformer is not respecting required attributes. For example, the type guard for this type:

export type RepoResource = {
  name: string;
  repoUrl: string;
  mountPath: string;
  packCommand: string;
  packName: string;
};

Translates as:

value => 
    (value === null) || 
    (value === undefined) || 
    (
        ((value !== undefined) && (value !== null)) && 
        ((value["name"] === null) || (value["name"] === undefined) || (typeof value["name"] === "string")) && 
        ((value["repoUrl"] === null) || (value["repoUrl"] === undefined) || (typeof value["repoUrl"] === "string")) && 
        ((value["mountPath"] === null) || (value["mountPath"] === undefined) || (typeof value["mountPath"] === "string")) && 
        ((value["packCommand"] === null) || (value["packCommand"] === undefined) || (typeof value["packCommand"] === "string")) && 
        ((value["packName"] === null) || (value["packName"] === undefined) || (typeof value["packName"] === "string")))

Thus, any missing attribute is skipped giving false positives:

isA<RepoResource>({ hi: 'there' }); // true
// or even more strange results:
return isA<RepoResource>('hello'); // true
return isA<RepoResource>(1); // true

Is there an option or something that I'm missing out?
I'm using

  • node v14.15.1
  • ts-type-checked 0.6.2
  • ttypescript v1.5.12
  • typescript v3.8.2

Thanks

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.