Functions have two places where you can annotate types: arguments (input) and the return value (output).
// Function Declarationfunction sum1(a: number, b: number): number {return a + b;}// Function Expressionconst sum2 = function (a: number, b: number): number {return a + b;};// Arrow Function Expressionconst sum3 = (a: number, b: number): number => a + b;let result = sum1(1, 2); // 👌!result = sum2(1, 2); // 👌!result = sum3(1, 2); // 👌!// Error: Type "false" is incompatible with type "number"result = sum1(false, "");// Error: Type "''" is incompatible with type "number"result = sum2("", 4);// Error: Type "'1'" is incompatible with type "number"result = sum3("1", "2");
Function Returns
As you understand, arguments types will be checked when you try to apply arguments to a function, but return type will be checked when you implement function logic.
function awesome(name: string): string {// Error: Type "2" is incompatible with type "string"return 2;}
Return types ensure that every return
will be called with the same type. This prevents you from accidentally not returning a value under certain conditions.
Optional and Default Argument
All arguments are required by default, but as was mentioned in Optional Types, if you annotate some argument as optional type then this argument will become optional:
function awesome(name: ?string) {const actualName = name === undefined ? "you" : name;return `Awesome, ${actualName}.`;}let result = awesome(); // 👌!result = awesome("JavaScript"); // 👌!
In JavaScript, we can also set a default value for optional arguments like this:
function awesome(name = "you") {return `Awesome, ${name}.`;}
So, which type should the name
argument be annotated with? You can see this demonstrated in the Hegel Playground.
If you hover over the function name and then on the argument name, you will see some magic; the function argument is string | undefined
outside the function but string
inside.
// awesome: (string | undefined) => stringfunction awesome(name: string = "you") {return `Awesome, ${name}.`;}let result = awesome(); // 👌!result = awesome("JavaScript"); // 👌!
Rest Argument
Sometimes, you don't know how many arguments will be applied to the function. JavaScript supports rest arguments. The rest operator '...' groups all applied arguments in an array of arguments.
function sum(...numbers) {// ...}
In Hegel you can annotate this argument the same as other arguments, but with a constraint. The type of this rest argument should be an Array
type or tuple type
function sum(...numbers: Array<number>) {return numbers.reduce((a, b) => a + b, 0);}let result = sum(); // 👌!result = sum(1); // 👌!result = sum(1, 2); // 👌!result = sum(1, 2, 42); // 👌!result = sum(1, 2, 42, 14); // 👌!// ...
function sum(...numbers: [number] | [number, number] | [number, number, number]) {return numbers.reduce((a, b) => a + b, 0);}// Error: Type "[]" is incompatible with type "...[number, number, number] | [number, number] | [number]"let result = sum();result = sum(1); // 👌!result = sum(1, 2); // 👌!result = sum(1, 2, 3); // 👌!// Error Type "[1, 2, 3, 4]" is incompatible with type "...[number, number, number] | [number, number] | [number]"result = sum(1, 2, 3, 4);
Function Types
Sometimes, you need to annotate arguments or return types as a function. Hegel has syntax for function type annotation (type1, type2) => returnType
. For example:
function twice(fn: (number) => number, arg: number) {return fn(fn(arg));}let result: number = 0;result = twice((a: number): number => a + 4, 4); // 👌!result = twice(// 👌!(a: number | string): number => (typeof a === "string" ? parseInt(a) : a) + 4,4);// Error: Type "(string) => number" is incompatible with type "(number) => number"result = twice((a: string): number => parseInt(a), 4);// Error: Type "(number) => string" is incompatible with type "(number) => number"result = twice((a: number): string => String(a), 4);// Error: Type "(number) => number | string" is incompatible with type "(number) => number"result = twice((a: number): number | string => a, 4);
If you play with the previous example you will see the next rule of the function type: You can assign one function to another only if the actual argument types are wider than declared and the return type is more specific than declared. This rule sounds like: function is contravariant by arguments and covariant by return.
A more classical and simple example of this rule:
class Animal {name: string;constructor(name: string) {this.name = name;}}class Dog extends Animal {}class Corgi extends Dog {}function presentMyDog(goodBoy: Dog, dogPresenter: (Dog) => Dog): string {const dog = dogPresenter(goodBoy);return `${dog.name}, my ${dog.name}, i will give you to noone.`;}const nickolay = new Corgi("Nickolay");let result = "";result = presentMyDog(nickolay, (goodBoy: Dog): Dog => goodBoy); // 👌!result = presentMyDog(nickolay, (goodBoy: Animal): Dog => nickolay); // 👌!// Error: Type "(Corgi) => Dog" is incompatible with type "(Dog) => Dog"result = presentMyDog(nickolay, (goodBoy: Corgi): Dog => nickolay);result = presentMyDog(nickolay, (goodBoy: Dog): Corgi => nickolay); // 👌!// Error: Type "(Dog) => Animal" is incompatible with type "(Dog) => Dog"result = presentMyDog(nickolay, (goodBoy: Dog): Animal => nickolay);
Function
type
Also, Hegel supports Function
type which represents any possible function in JavaScript:
function argsLength(fn: Function) {return fn.length;}let length = 0;length = argsLength(() => 2);length = argsLength((a: number) => a);length = argsLength(parseFloat);// Error: Type "2" is incompatible with type "Function"length = argsLength(2);// Error: Type "[]" is incompatible with type "Function"length = argsLength([]);