A JavaScript argument-type-checker, without the need for preprocessing or compiling.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
js
test
.gitignore
LICENSE
README.md
TODO
_config.yml
package.json

README.md

argtyper

JS Type-Constraints without compilation.

Type checking is very useful in JavaScript - it helps catch a number of different bugs. However, to actually use type checking normally, you'd install a library such as Flow, which is great, but you have to compile your program. This not only takes time, but is annoying to set up.

argtyper is a better way to implement type checking for function arguments and return types.

Features

  • Argument types
  • Function return types
  • Type aliases -- e.g. Vector{ x: Number, y: Number }
  • Deep array and object typing -- e.g. [Number, [Number, [String, Number]]]
  • Polymorphic types -- e.g. Number | String → Either a number or a string
  • Repeated types -- e.g. [Number * 5] → An array of five numbers
  • Arrays of any length -- e.g. [...Number] → An array of any amount of numbers

Installation

If you're using npm, you can just run this command to install argtyper:

$ npm install --save argtyper

And to import the type function into a file, use the following:

var type = require('argtyper').type

You can then just call type on functions, as documented in the example below.

Example

Here is a basic program using argtyper. In it, a simple function, called add, is defined, with two arguments, called a and b. Both of them are Number's : the allowed type is written following an equals, after the name of the argument.

function add(a=Number, b=Number) {
  return a + b
}

add = type(add)

The last line is very important. The type function wraps a given function in some type checking code and returns the new one.

Below are some test cases, showing what happens when add is executed with different arguments.

// Test cases

add(5, 3)                //=> 8
add(5, true)             //=> Error
add(6, 1, 8)             //=> Error
add(7)                   //=> Error

It also works with arrow functions, which can be useful to make typed functions a little faster to write. The function above can be rewritten as the following:

var add = type((a=Number, b=Number) => {
  return a + b;
});

Typing objects

argtyper also has a function to type all functions in an object, called typeAll(). It takes one argument: object, which is the object to type.

To import it, use the following:

var typeAll = require('argtyper').typeAll

And here's how to use it:

const maths = {
  add = function (a=Number, b=Number) {
    return a + b;
  },
  mul = function (a=Number, b=Number) {
    return a * b
  }
};

typeAll(maths) // Note you don't have to assign it back to the object

maths.add(5, 5)    //=> 10
maths.mul(5, 5)    //=> 25
maths.add('2', 10) //=> Error

As kind of demonstrated in that example, the typeAll function can be very useful if you want to type a module, if that module is defined as properties of an object.

Documentation

Syntax overview

Function syntax

The simplest sort of typing argtyper supports is typing the arguments of a function, as demonstrated above:

function fn (a=Number, b=String) {
  ...
}

fn = type(fn) // Remember to call 'type'!

This function would only accept two arguments. The first would would have to be a number, and the second would have to be a string. If any of these requirements were not met, an error would be thrown.


You can also give a function a return type. This is only possible with arrow functions, due to the syntax of them:

let add = (a=Number | String, b=Number | String) => String => {
  return a + b
}

add = type(add)

Don't worry about the Number | String notation, it will be explained in the next section.

This function takes two arguments, a and b, both of them being either a number or a string. The function can, however, only return a string - otherwise it will return an error


If you want to use a more complex constraint as the return type, there are two ways of doing so.

Firstly, you could use (_=Constraint) notation:

let fn = (...) => (_=Number | String) => {
  ...
}

This means you can use any of the constraints defined below, because without using this syntax, javascript's grammar won't allow your function.

The second way is to define an alias for your complex type, turning it into a simple identifier:

typedef(Vector => { x: Number, y: Number })

Then, you can just define your function like:

let fn = (...) => Vector => {
  ...
}

Constraints

There are many different types of constraints in argtyper. Here's a list!

  • ClassName - The simplest constraint, which you've already seen in the earlier examples, is just a single class name, such as Number or String.

    • Don't use Array or Object as a class name, as they are to be written using their own syntax (described below.)
    • Also don't use Any, because it's it's own separate thing.
    • Example: Number allows a number
  • [Constraint, Constraint ...] - When having an array as an argument to a function, you specify the types for each element of that array. The syntax is very similar to defining a normal array, except each element of the constraining array should be another constraint (i.e. a class name, another array, etc...)

    • Example: [Number, String, [Number, Number, Number]] allows an array where the first element is a number, the second a string, and the third another array containing three numbers.
  • {x: Constraint, y: Constraint ...} - You can add an object constraint in a similar fashion to an array. Again, you use the exact same syntax as writing an object, just each property's value should be another constraint.

    • Example: {x: Number, y: Number} allows a two-dimensional vector in the form of an object with an x and y field, both numbers.
  • Constraint | Constraint[ | Constraint ...] - It's also possible for an argument to accept multiple types, using the | operator. It's fairly simple - here's an example:

    • Example: Number | String | Boolean allows a number, string, or boolean
  • Any - The word Any on its own just allows anything through. It's how you can make an untyped argument in argtyper.

  • [Constraint * amount] - Is the same as writing [Constraint, Constraint ... (amount times)].

    • Example: [Number * 10] allows an array of 10 numbers
  • [...Constraint] - A list of any size (except from 0) containing only Constraints.

    • Example: [...String] allows an array of any size > 0 of strings

Aliases - typedef(Name => Constraint)

Say you're writing a game. You'd probably use a lot of Vectors for velocity, position etc... In argtyper, you might represent a vector similar to the following:

function addThreeVectors (
  a={x: Number, y: Number},
  b={x: Number, y: Number},
  c={x: Number, y: Number}
) {
  ...
}

But as you can see, it just takes too long to write. And imagine if you repeatedly used an object which has, say, 10 properties. It'd just take too long. There must be a better way, right? Well, there is. You can use the typedef function, exported from argtyper:

var typedef = require('argtyper').typedef

This is a very useful little function. Here's an example of its use:

typedef(Vector => ({x: Number, y: Number}))

function addThreeVectors (a=Vector, b=Vector, c=Vector) {
  ...
}

Much nicer! And also exactly 100% identical to the previous function, as the aliases are automatically expanded upon parsing.

Now, not only can you make an alias for an object (like the example above) but you can actually make an alias for any of the following types:

  • Objects
  • Arrays
  • Actually, anything you can write as a constraint normally
  • Even other aliases

Here are some (completely trivial) examples using a some alias types mentioned above:

typedef(ThreeNumbers => [Number, Number, Number])
typedef(AddOperand => Number | String)
typedef(ThreeAddOperands => [AddOperand, AddOperand, AddOperand])

function sumThree (a=ThreeNumbers) {
  return a[0] + a[1] + a[2]
}

function add (a=AddOperand, b=AddOperand) {
  return a + b
}

function addThree (ops=ThreeAddOperands) {
  return ops[0] + ops[1] + ops[2]
}

(I didn't call type on the functions defined. In real life, you'd need to, but to make it more readable I didn't in this example. I also probably won't in other examples)

Aliases to shorten type names

Here's another really useful use of aliases:

typedef(N => Number)

As I mentioned earlier, aliases can be defined as any valid constraint. Therefore, you can also use them to just shorten class names:

function add (a=N, b=N) {
  return a + b
}

Repetition - [Constraint * n]

Sometimes, you want an array with lots of elements in it. Here's an example:

function sumOneHundred (a=[Number, Number, Number ... Number]) {
  return a.reduce((a, b) => a + b, 0)
}

(I omitted 96 Numbers, but you can imagine how long it'd be if I wrote them all out)

There's a better way to do this, of course. You can use the * operator:

function sumOneHundred (a=[Number * 100]) {
  return a.reduce((a, b) => a + b, 0)
}

As you can see, this looks a lot better.

To pass the one hundred arguments to this function, you'd do the following:

sumOneHundred([7, 2, 3, 10, 4, ...])

But obviously just a whole lot more elements in the array.

Infinite repetition

You can also define a constraint which matches a list of any size greater than 0 using the spread (...) operator:

function sumN (xs=[...Number]) {
  return xs.reduce((a, b) => a + b, 0)
}

sumN([1, 2, 3]) //=> 6
sumN([1])       //=> 1
sumN([])        //=> Error

If you also want to allow an array of length 0, you can use the following hack, until I add a special syntax for it:

function sumN (xs=[] | [...Number]) {
  return xs.reduce((a, b) => a + b, 0)
}

Which works because it can either match an empty array or an array with some numbers in it.

Problems

Well, "Problem". Basically, argtyper can be quite slow, so it's probably best not to use it for applications which need to be very performant.