argtyper
v2.4.7
Published
JS type-checker - no compilation!
Downloads
15
Readme
argtyper
This project is hosted on GitHub
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.
- Example:
{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.
- Example:
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
- Example:
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
- Example:
[...Constraint]
- A list of any size (except from 0) containing onlyConstraint
s.- Example:
[...String]
allows an array of any size > 0 of strings
- Example:
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 Number
s, 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.