Typecheck.js is a JavaScript library that lets you type check function parameters and return values at runtime. No compilation is necessary, any Typecheck.js code is valid JavaScript, and all type checking is done at runtime. It's compatible with both browser-based JavaScript and Node.js.
- Setup
- Basic usage
- List of types
- Containers, generics and tuples
- Special functions
- Scope of user-defined types
- The
typechecked.isinstance
function
In browser-based JavaScript, the easiest way to use Typecheck.js is to include it in your HTML file:
<script type="text/javascript" src="https://gustavlindberg99.github.io/Typecheck.js/min/typecheck-v1.min.js"></script>
This allows you to use Typecheck.js in any JavaScript files on that web page.
If you're using modules, it's also possible to import Typecheck.js using the import
keyword:
import typechecked from "https://gustavlindberg99.github.io/Typecheck.js/min/typecheck-v1.min.js";
To use Typecheck.js in Node.js, download typecheck.js into your project folder, then include it as follows:
const typechecked = require("./typecheck.js");
Typecheck.js creates a decorator called typechecked
which can be applied to functions and classes. Since the decorator proposal isn't implemented yet, you need to call the decorator and overwrite the function/class you're calling it on like this:
function f(){}
f = typechecked(f);
An alternative syntax to this is the following (the f
in function f
is not required, but it's recommended so that the function name shows up correctly in the debugger):
const f = typechecked(function f(){});
Typecheck.js does two things: checks that the number of parameters of a function match the number of parameters specified in its declaration, and checks that the parameters and return type are of the correct type.
To check the number of parameters, you don't need to do anything special other than applying the typechecked
decorator:
function noArgs(){}
noArgs = typechecked(noArgs);
noArgs(); //OK
noArgs(1); //TypeError: too many arguments
function oneArg(a){}
oneArg = typechecked(oneArg);
oneArg(); //TypeError: too few arguments
oneArg(1); //OK
oneArg(1, 2); //TypeError: too many arguments
Arrow functions only check if too few parameters are specified, not if too many parameters are specified:
const arrowFunction = typechecked((a) => {});
arrowFunction(); //TypeError: too few arguments
arrowFunction(1); //OK
arrowFunction(1, 2); //OK
arrowFunction(1, 2, 3); //OK
arrowFunction(1, 2, 3, 4); //OK
This is because arrow functions are often used as callbacks, and sometimes the caller specifies parameters that aren't needed. For example, the callback to the Array.map
method can take up to three parameters, but often only one is needed, so without this feature, the following code wouldn't be possible:
[1, 2, 3].map(typechecked(x => x 1));
However, it is often useful to check that the parameters and return values of a function are of a specific type. Since JavaScript has no native syntax for this, the type declarations are placed in comments of the form /*: Type */
:
function square(x /*: Number */) /*: Number */ {
return x**2;
}
square = typechecked(square);
square(4); //OK
square("Hello World!"); //TypeError: expected parameter to be Number, got String
The return type is also checked:
function square(x /*: Number */) /*: String */ {
return x**2;
}
square = typechecked(square);
square(4); //TypeError: expected return type to be String, got Number
Note that there should never be a space between /*
and :
. A comment such as /* : Number */
will be ignored and treated as a regular comment. The spaces between /*:
, the type and */
are however optional, so /*:Number*/
is OK.
In case you're wondering how Typecheck.js can make comments change the functionality of the code, the toString()
method of functions preserves comments, so functions can be converted to strings and then the comments can be parsed.
Using typechecked
on a class automatically typechecks all of its public methods, including its constructor, getters, setters and static methods. Example:
class MyClass{
constructor(x /*: Number */){}
myMethod() /*: void */ {}
}
MyClass = typechecked(MyClass);
const a = new MyClass(4); //OK
a.myMethod(); //OK
a.myMethod(3); //TypeError, too many arguments
const b = new MyClass(""); //TypeError, wrong type passed to constructor
Note that constructors should never have a return type. Specifying a return type to a constructor will throw a SyntaxError when calling typechecked
.
This only typechecks public methods, it doesn't typecheck its private methods. This is because since typechecked
is defined outside of the class, it doesn't have any way of accessing any private methods. Unfortunately, there is currenlty no workaround for this, so private methods can't be typechecked. However, when the decorator proposal becomes implemented, it will be possible to use @typechecked
as a decorator on any methods, public or private.
In typecheck.js, you can use classes in type declarations as well as a few special types. The special types are all reserved keywords on purpose so that they can't conflict with user-defined classes.
Any class can be used in a type declaration, as long as it's either a member of globalThis
, is itself typechecked
, or has been added through typechecked.add
. This works for all built-in types and most user-defined types. In non-module scripts, user-defined classes in the global namespace are always members of globalThis
, so they can be used in type declarations without any restrictions. In modules, however, user-defined classes may need to be typechecked
for it to work, see the Modules section below.
This works for built-in types as well, however, the primitive classes Number
, String
, Boolean
, Symbol
and BigInt
check that the variable is of the corresponding primitive type and not an object. There is no way to type check for wrapper objects since they're not very useful. For example:
function square(x /*: Number */) /*: Number */ {
return x**2;
}
square = typechecked(square);
square(4); //OK
square(new Number(4)); //TypeError
Other built-in types (such as RegExp
, Date
, XMLHttpRequest
, etc) simply check if it's an instance of that class using isinstance
. Built-in container types (Array
, Set
, Map
) can also be used as usual, but also have the possibility to be used as generics, see the Generics section below.
Since the typechecking uses isinstance
, instances of a derived class are also considered to be instances of a base class. For example, an HTMLBodyElement
object is considered to also be an HTMLElement
object, and if you have class Base{}
and class Derived extends Base{}
, a Derived
object is also considered to be a Base
object. Also, this means that everything except null or undefined is considered to be an instance of Object
(including primitives and wrapper objects).
Even though typeof NaN === "number"
, Typecheck.js does not consider NaN to be an instance of Number
. The reason for this is because NaN often indicates that a calculation has gone wrong, and the point of Typecheck.js is to catch errors early instead of allowing them to propagate to unrelated parts of the code. If you want to specify that a variable should be a number including NaN, you can use Number | NaN
(NaN
is a special type that checks for NaN, see below).
The special type null
can be used to check that a variable is null
. This is mostly useful in union types, so to check that a varaible is either an instance of SomeClass
or is null, you can use SomeClass | null
. Similarly, undefined
checks that a variable is undefined, and NaN
checks that a variable is NaN (i.e. typeof obj === "number" && isNaN(obj)
).
void
also checks that the return value of a function is undefined, but unlike undefined
, it can only be used as a return type, and can't be used in union types or generics. It's intended to be used to indicate that a function returns nothing. Since in JavaScript functions that return nothing return undefined, a return type of void void
is functionally identical to a plain return type of undefined
, but they are intended to have different meanings: void
is indended to be used as the return type of functions that return nothing, and undefined
is intended to be used for other uses of undefined.
Since Function
is a member of globalThis
, it can be used to type check for functions. However, this is rarely useful since in JavaScript a "function" can mean many different things. For example, classes and arrow functions are both Function
objects, but are almost never used in the same way. For this reason, typecheck.js has several special function types:
class
, checks that the variable can be called withnew
. This is true for ES6 classes as well as regular function defined using thefunction(){}
syntax, but not for arrow functions, async functions or generator functions.function
, checks that the variable is a function that can be called withoutnew
, i.e. a function that's not an ES6 class. Note that functions defined using thefunction(){}
syntax are considered both aclass
and afunction
, since they can be called both with and withoutnew
. Not to be confused withFunction
, which simply checks that it's an instance ofFunction
, i.e. either a function or a class.async
, checks that the variable is an async function and not a generator function.function*
, checks that the variable is a generator function and not an async function.async*
, checks that the variable is both an async function and a generator function.
Note that Function
is equivalent to class | function
. However, class | function
is more readable since it more explicitly states that both classes and functions are acceptable.
The special type var
does no type checking at all, it's equivalent to not having any type declaration. It can't be used in union types (since otherwise a union type including var
would be equivalent to var
itself). Since Object
checks for anything that's not null or undefined (see above), var
is functionally equivalent to Object | null | undefined
. However, var
has better performance.
There are a few reasons to use var
over no type declaration at all:
- Readability. If you see a function with no type declaration, it can be hard to tell if the type declaration was left out for some other reason (for example by mistake or to make the function declaration shorter). If you use
/*: var */
, it's immediately clear that the variable can have any type. - In some cases, for example in generics, it's not possible to omit
var
. For example, if you want a map whose keys are strings and whose values can be of any type, the only way of doing isMap<String, var>
.
Sometimes you might want to check that a variable has one of several types. You can do this by separating the types with a |
. To check that a variable is either of Type1
or of Type2
, you can do Type1 | Type2
. You can have as many types as you want in a union type.
This is most often useful with null
. To check that a varaible is either an instance of SomeClass
or is null, you can use SomeClass | null
.
The built-in types Array
, Set
and Map
can be used like any other classes in type declarations. However, often it's not enough to know that a variable is an array, set or map, you also want to know what it contains. For this reason, Array
, Set
and Map
can also be used with generics.
It is not possible to create your own generics in Typecheck.js, generics can only be used with the built-in types Array
, Set
and Map
.
The syntax Array<Type>
checks not only that the variable is an array, but also that all its elements are of type Type
. Similarly Set<Type>
checks that the variable is a set and that all its elements are of type Type
. Type
can be any valid typecheck.js type, including a union type or a generic.
So for example to check that all the elements of the array are either instances of SomeClass
or is null, you can do Array<SomeClass | null>
. Nested generics are also allowed, for example Array<Array<Type>>
.
Note that Array<var>
is equivalent to just Array
.
A map has both keys and values, so to check that a variable is a map, that all keys are of type KeyType
and that all values are of type ValueType
, you can do Map<KeyType, ValueType>
. Again, KeyType
and ValueType
can be any valid typecheck.js types, including union types or generics.
If you only want to type check the keys or the values but not both, this is where var
is useful. Map<KeyType, var>
only typechecks the keys, and Map<var, ValueType>
only type checks the values.
You can also do type checking on rest parameters. Since rest parameters are always arrays, the most useful way to type check them is to use an array generic. For example:
//Allows any number of parameters, but checks that they're all numbers
//The parameters will be stored as an array
function f(...a /*: Array<Number> */){}
f = typechecked(f);
f(); //OK
f(1); //OK
f(1, 2, 3); //OK
f("Hello World!"); //TypeError
Sometimes you need arrays that have a specific number of elements with specific types. This is especially useful for return values, since JavaScript functions can only have one return value. For this you can use tuples, which have the syntax [Type1, Type2, ...]
:
function multipleReturnValues() /*: [String, Number, Boolean] */ {
return ["Hello World!", 4, true];
}
multipleReturnValues = typechecked(multipleReturnValues);
const [myString, myNumber, myBoolean] = multipleReturnValues();
Tuples can also be useful in combination with rest parameters and union types to overload functions. For example, the following function has two overloads, one that takes two numbers and one that takes one string:
function overloadedFunction(...args /*: [Number, Number] | [String] */){
if(typechecked.isinstance(args, "[Number, Number]")){
const [x, y] = args;
//Overload with two numbers
}
else{
const [myString] = args;
//Overload with one string
}
}
overloadedFunction = typechecked(overloadedFunction);
Arrow functions can be typechecked just like regular functions, but you can only use type declarations in arrow functions if the parameter list is enclosed in parentheses (there is no restriction on whether or not the body should be enclosed in curly braces). The return type is placed between the parameter list and the arrow.
Examples:
//Regular arrow function with both parentheses and curly braces
typechecked((a /*: Number */, b /*: String */) /*: void */ => {}); //OK
//Omitting the curly braces is allowed
typechecked((a /*: Number */, b /*: String */) /*: Boolean */ => b.length === a); //OK
//Omitting the parentheses around the parameters not allowed if there are type declarations
typechecked(a /*: Number */ => a); //Ambiguous: `(a /*: Number */) => a`
//or `(a) /*: Number */ => a`?
//If there aren't type declarations, omitting the parentheses is allowed
//The following just checks the number of parameters, not the types
typechecked(a => a); //OK
Async functions always return Promise
objects and generator functions always return Generator
objects. Since checking the return types of these functions for Promise
or Generator
would be redundant, the return type of any function declared with the async
keyword instead checks the contents of the promise, and the return type of any function declared with the function*
keyword instead checks the contents of the generator:
async function myAsyncFunction() /*: Number */ {
return 3;
}
myAsyncFunction = typechecked(myAsyncFunction);
function* myGeneratorFunction() /*: Number */ {
yield 4;
}
myGeneratorFunction = typechecked(myGeneratorFunction);
Note that the return type of async functions is only checked once the function returns:
async function myAsyncFunction() /*: String */ {
await new Promise(r => setTimeout(r, 1000));
return 3;
}
myAsyncFunction = typechecked(myAsyncFunction);
myAsyncFunction(); //Only throws a TypeError after 1 second
Similarly, the return type of generator functions is only checked once the function yields:
function* myGeneratorFunction() /*: String */ {
yield 4;
}
myGeneratorFunction = typechecked(myGeneratorFunction);
let generator = myGeneratorFunction(); //No error
generator.next(); //TypeError: expected yield value to be String, got Number
The parameter types of async and generator functions are checked just like any other functions, immediately when the function gets called.
As stated above, you can type check for any class name that's either a member of globalThis
, that's typechecked, or that has been added through typechecked.add, including user-defined classes.
In non-module scripts, any class defined in the global namespace works, since it's a member of globalThis
:
<script type="text/javascript">
class MyClass{}
//Can be typechecked if you want, but doesn't need to to be useable in type declarations
function f(a /*: MyClass */){}
f = typechecked(f);
f(new MyClass()); //OK
f("Hello World!"); //TypeError: wrong type
</script>
However, if you're using modules, it's no longer that simple, since classes defined in the global namespace in modules aren't automatically members of globalThis
:
<script type="module">
class MyClass{}
function f(a /*: MyClass */){}
f = typechecked(f);
f(new MyClass()); //ReferenceError: typechecked doesn't know what MyClass is
</script>
The reason this doesn't work is because MyClass
can only be accessed in the current module, and typechecked
which is trying to access it is defined outside of the module.
However, if MyClass
is typechecked, typechecked
has access to it since it was passed to it earlier, and so it can be used as usual:
<script type="module">
class MyClass{}
MyClass = typechecked(MyClass);
function f(a /*: MyClass */){}
f = typechecked(f);
f(new MyClass()); //OK since MyClass is typechecked
</script>
Because of this, however, you can't define two typechecked classes with the same name, not even in different modules.
<script type="module">
class MyClass{}
MyClass = typechecked(MyClass);
</script>
<script type="module">
class MyClass{}
MyClass = typechecked(MyClass); //ReferenceError: Redefinition of MyClass
</script>
This is because typechecked
doesn't know which module it's being called from, so it doesn't know which one to choose.
If you want to use a class in type declarations that's not a member of globalThis
but you don't want to make it typechecked (for example because it's a class from a third-party library), you can make it known to typechecked by using typechecked.add
. typechecked.add
takes any number of classes as parameters, and after it has been called, you can use it in type declarations just like any other class.
Example:
import {ThirdPartyClass1, ThirdPartyClass2} from "http://example.com/third-party-library.js";
function f(x /*: ThirdPartyClass1 */, y /*: ThirdPartyClass2 */){}
f = typechecked(f);
f(new ThirdPartyClass1(), new ThirdPartyClass2()); //Error: typecheck.js doesn't know about these classes
typechecked.add(ThirdPartyClass1, ThirdPartyClass2);
f(new ThirdPartyClass1(), new ThirdPartyClass2()); //OK, they have been added with typechecked.add
Similarly, local classes can't either be used in type declarations since they're not either members of globalThis
, unless they're typechecked (regardless of whether the script is a module or not). However, you need to be careful with anonymous classes:
function outer(){
class LocalClass1{};
let inner = typechecked((a /*: LocalClass1 */) => {});
inner(new LocalClass()); //ReferenceError: typechecked can't access LocalClass1.
const LocalClass2 = typechecked(class{});
inner = typechecked((a /*: LocalClass2 */) => {});
inner(new LocalClass2()); //ReferenceError: while typechecked can access
//LocalClass2, it doesn't know it's called that
//since it was declared as an anonymous class.
const LocalClass3 = typechecked(class LocalClass3{});
inner = typechecked((a /*: LocalClass3 */) => {});
inner(new LocalClass3()); //OK, LocalClass3 was declared with that name
//and is typechecked. However, you need to be
//careful not to have classes called LocalClass3
//anywhere else.
}
If a class is a member of another class, it's possible to typecheck for the inner class using the syntax OuterClass.InnerClass
, as long as the outer class fulfills the requirements above. Example:
class Outer{
static Inner = class{}
}
Outer = typechecked(Outer);
function f(x /*: Outer.Inner */){}
f = typechecked(f);
f(new Outer.Inner());
If you want to typecheck using Typecheck.js syntax elsewhere than in function paremeters and return types, you can use the typechecked.isinstance
function, which has the following signature:
function typechecked.isinstance(
obj /*: var */,
type /*: String | class | null | undefined | NaN */
) /*: Boolean */
This function has the following behavior depending on the type of type
:
- If
type
is a string, parses it as a Typecheck.js type, then returns true ifobj
is an instance of that type and false otherwise. Throws an error if the parsing failed. - If
type
is null, returns true ifobj === null
and false otherwise. - If
type
is undefined, returns true ifobj === undefined
and false otherwise. - If
type
is NaN, returns true ifobj
is NaN and false otherwise. - If
type
isNumber
,String
,Boolean
,Symbol
orBigInt
, returns true iftypeof obj
isnumber
,string
,boolean
,symbol
orbigint
respecitvely, and false otherwise. - If
type
isObject
, returns true ifobj !== null
andobj !== undefined
, and false otherwise. - If
type
is any other class, returns the result ofobj isinstance type
.