loading...

TypeScript Function Annotations

spukas profile image Linas Spukas ・3 min read

TypeScript has a robust inference system, which automatically assigns the best possible data type for values. It works very well for variables, objects, arrays and more, so you do not need to think about writing annotations for them. But for functions, inference evaluates only a return value and assign best possible type candidate for it. That means function arguments are not evaluated, and we need to do it by writing annotations manually.

Function annotations can be written in couple of ways. First you can annotate a variable as a function type (which is more variable annotation then a function):

const myFunction: (name: string) => void = (name) => console.log(name);

and a regular function annotation, which I will focus on:

const myFunction = (name: string): void => console.log(name);

Don't trust inference

Coming back to the inference system, I want to show why we should not rely on it for functions. Take the following picture, for example, where a function receives two strings as arguments, first and second name, and returns a full name. I will not add annotations on purpose, see how the inference evaluates the functions. Firstly, TypeScript cannot determine which types used for function arguments, so they are marked as any type, which we try to avoid:

Alt Text

But the function return statement was evaluated and data type assigned correctly:

Alt Text

Furthermore, if you accidentally forgot a return statement, then will get a void type and no errors:

Alt Text

That is why to trust inference for functions is not an ideal and very error-prone way.

Writing annotations

By writing annotations for arguments and return values, we help TypeScript to prevent errors and notify us early if we try to use different types.
The syntax for a different kind of functions is pretty much the same: (arg: Type): Type. Annotations for arguments go into the parentheses, and return type follows right after them.

Arrow functions:

Taken the same example from above, we would annotate it this way:

const getFullName = (firstName: string, secondName: string): string => {
   return firstName + ' ' + secondName;
}

Now if you accidentally forget a return statement, you will be notified that function should return, and it must be a string. Same goes for both arguments.

Function declarations:

function multiply(x: number, y: number): number {
    return x * y;
}

Function expressions:

const multiply = function(x: number, y: number): number {
    return x * y;
}

Destructuring function arguments

To annotate an object as function argument you would follow the same object literal annotation, only in a function parentheses:

const logPerson = (person: { name: string; age: number }) => {
    console.log('name:', person.name, 'age:', person.age);
}

And if you prefer argument destructuring, it goes with the same syntax:

const logPerson = ({ name, age }: { name: string; age: number }) => {
    console.log('name:', name, 'age:', age);
}

Also, I should mention a function return type never, which is used for sporadic cases. It makes sure the function will never reach its end. A use case for it if an error will be thrown inside the function:

function neverReachAnEnd(): never {
    throw new Error();
}

Conclusion

While the inference system works very well for other data types, do not trust it for functions. Write annotations manually to help TypeScript understand what we intend to pass for function arguments and what is expected return statement. This way, we make functions less error-prone and get early notifications for any type mismatch.

Posted on by:

spukas profile

Linas Spukas

@spukas

Full-stack web developer with a specialisation in React and NodeJS.

Discussion

pic
Editor guide