Software Engineer - Lindy.ai
The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.
Creating and using functions is a fundamental aspect of any programming language, and TypeScript is no different. TypeScript fully supports the existing JavaScript syntax for functions, while also adding type information and function overloading as new features. Besides providing extra documentation to the function, type information also reduces the chances of bugs in the code because there’s a lower risk of passing invalid data types to a type-safe function.
In this tutorial, you will start by creating the most basic functions with type information, then move on to more complex scenarios, like using rest parameters and function overloading. You will try out different code samples, which you can follow in your own TypeScript environment or the TypeScript Playground, an online environment that allows you to write TypeScript directly in the browser.
To follow this tutorial, you will need:
tsc
) installed on your machine. To do this, refer to the official TypeScript website.All examples shown in this tutorial were created using TypeScript version 4.2.2.
In this section, you will create functions in TypeScript, and then add type information to them.
In JavaScript, functions can be declared in a number of ways. One of the most popular is to use the function
keyword, as is shown in the following:
function sum(a, b) {
return a + b;
}
In this example, sum
is the name of the function, (a, b)
are the arguments, and {return a + b;}
is the function body.
The syntax for creating functions in TypeScript is the same, except for one major addition: You can let the compiler know what types each argument or parameter should have. The following code block shows the general syntax for this, with the type declarations highlighted:
function functionName(param1: Param1Type, param2: Param2Type): ReturnType {
// ... body of the function
}
Using this syntax, you can then add types to the parameters of the sum
function shown earlier:
function sum(a: number, b: number) {
return a + b;
}
This ensures that a
and b
are number
values.
You can also add the type of the returned value:
function sum(a: number, b: number): number {
return a + b;
}
Now TypeScript will expect the sum
function to return a number value. If you call your function with some parameters and store the result value in a variable called result
:
const result = sum(1, 2);
The result
variable is going to have the type number
. If you are using the TypeScript playground or are using a text editor that fully supports TypeScript, hovering over result
with your cursor will show const result: number
, showing that TypeScript has implied its type from the function declaration.
If you called your function with a value that has a type other than the one expected by your function, the TypeScript Compiler (tsc
) would give you the error 2345
. Take the following call to the sum
function:
sum('shark', 'whale');
This would give the following:
OutputArgument of type 'string' is not assignable to parameter of type 'number'. (2345)
You can use any type in your functions, not just basic types. For example, imagine you have a User
type that looks like this:
type User = {
firstName: string;
lastName: string;
};
You could create a function that returns the full name of the user like the following:
function getUserFullName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
Most of the times TypeScript is smart enough to infer the return type of functions, so you can drop the return type from the function declaration in this case:
function getUserFullName(user: User) {
return `${user.firstName} ${user.lastName}`;
}
Notice you removed the : string
part, which was the return type of your function. As you are returning a string in the body of your function, TypeScript correctly assumes your function has a string return type.
To call your function now, you must pass an object that has the same shape as the User
type:
type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User) {
return `${user.firstName} ${user.lastName}`;
}
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
This code will successfully pass the TypeScript type-checker. If you hover over the userFullName
constant in your editor, the editor will identify its type as string
.
Having all parameters is not always required when creating functions. In this section, you will learn how to mark function parameters as optional in TypeScript.
To turn a function parameter into an optional one, add the ?
modifier right after the parameter name. Given a function parameter param1
with type T
, you could make param1
an optional parameter by adding ?
, as highlighted in the following:
param1?: T
For example, add an optional prefix
parameter to your getUserFullName
function, which is an optional string that can be added as a prefix to the user’s full name:
type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User, prefix?: string) {
return `${prefix ?? ''}${user.firstName} ${user.lastName}`;
}
In the first highlighted part of this code block, you are adding an optional prefix
parameter to your function, and in the second highlighted part you are prefixing the user’s full name with it. To do that, you are using the nullish coalescing operator ??
. This way, you are only going to use the prefix
value if it is defined; otherwise, the function will use an empty string.
Now you can call your function with or without the prefix parameter, as shown in the following:
type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User, prefix?: string) {
return `${prefix ?? ''} ${user.firstName} ${user.lastName}`;
}
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
const mrUserFullName = getUserFullName(user, 'Mr. ');
In this case, the value of userFullName
will be Jon Doe
, and the value of mrUserFullName
will be Mr. Jon Doe
.
Note that you cannot add an optional parameter before a required one; it must be listed last in the series, as is done with (user: User, prefix?: string)
. Listing it first would make the TypeScript Compiler return the error 1016
:
OutputA required parameter cannot follow an optional parameter. (1016)
So far, this tutorial has shown how to type normal functions in TypeScript, defined with the function
keyword. But in JavaScript, you can define a function in more than one way, such as with arrow functions. In this section, you will add types to arrow functions in TypeScript.
The syntax for adding types to arrow functions is almost the same as adding types to normal functions. To illustrate this, change your getUserFullName
function into an arrow function expression:
const getUserFullName = (user: User, prefix?: string) => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
If you wanted to be explicit about the return type of your function, you would add it after the ()
, as shown in the highlighted code in the following block:
const getUserFullName = (user: User, prefix?: string): string => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
Now you can use your function exactly like before:
type User = {
firstName: string;
lastName: string;
};
const getUserFullName = (user: User, prefix?: string) => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
This will pass the TypeScript type-checker with no error.
Note: Remember that everything valid for functions in JavaScript is also valid for functions in TypeScript. For a refresher on these rules, check out our How To Define Functions in JavaScript tutorial.
In the previous sections, you added types to the parameters and return values for functions in TypeScript. In this section, you are going to learn how to create function types, which are types that represent a specific function signature. Creating a type that matches a specific function is especially useful when passing functions to other functions, like having a parameter that is itself a function. This is a common pattern when creating functions that accept callbacks.
The syntax for creating your function type is similar to creating an arrow function, with two differences:
return
type itself.Here is how you would create a type that matches the getUserFullName
function you have been using:
type User = {
firstName: string;
lastName: string;
};
type PrintUserNameFunction = (user: User, prefix?: string) => string;
In this example, you used the type
keyword to declare a new type, then provided the type for the two parameters in the parentheses and the type for the return value after the arrow.
For a more concrete example, imagine you are creating an event listener function called onEvent
, which receives as the first parameter the event name, and as the second parameter the event callback. The event callback itself would receive as the first parameter an object with the following type:
type EventContext = {
value: string;
};
You can then write your onEvent
function like this:
type EventContext = {
value: string;
};
function onEvent(eventName: string, eventCallback: (target: EventContext) => void) {
// ... implementation
}
Notice that the type of the eventCallback
parameter is a function type:
eventCallback: (target: EventTarget) => void
This means that your onEvent
function expects another function to be passed in the eventCallback
parameter. This function should accept a single argument of the type EventTarget
. The return type of this function is ignored by your onEvent
function, and so you are using void
as the type.
When working with JavaScript, it is relatively common to have asynchronous functions. TypeScript has a specific way to deal with this. In this section, you are going to create asynchronous functions in TypeScript.
The syntax for creating asynchronous functions is the same as the one used for JavaScript, with the addition of allowing types:
async function asyncFunction(param1: number) {
// ... function implementation ...
}
There is one major difference between adding types to a normal function and adding types to an asynchronous function: In an asynchronous function, the return type must always be the Promise<T>
generic. The Promise<T>
generic represents the Promise object that is returned by an asynchronous function, where T
is the type of the value the promise resolves to.
Imagine you have a User
type:
type User = {
id: number;
firstName: string;
};
Imagine also that you have a few user objects in a data store. This data could be stored anywhere, like in a file, a database, or behind an API request. For simplicity, in this example you will be using an array:
type User = {
id: number;
firstName: string;
};
const users: User[] = [
{ id: 1, firstName: "Jane" },
{ id: 2, firstName: "Jon" }
];
If you wanted to create a type-safe function that retrieves a user by ID in an asynchronous way, you could do it like this:
async function getUserById(userId: number): Promise<User | null> {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
In this function, you are first declaring your function as asynchronous:
async function getUserById(userId: number): Promise<User | null> {
Then you are specifying that it accepts as the first parameter the user ID, which must be a number
:
async function getUserById(userId: number): Promise<User | null> {
The return type of getUserById
is a Promise that resolves to either User
or null
. You are using the union type User | null
as the type parameter to the Promise
generic.
User | null
is the T
in Promise<T>
:
async function getUserById(userId: number): Promise<User | null> {
Call your function using await
and store the result in a variable called user
:
type User = {
id: number;
firstName: string;
};
const users: User[] = [
{ id: 1, firstName: "Jane" },
{ id: 2, firstName: "Jon" }
];
async function getUserById(userId: number): Promise<User | null> {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
async function runProgram() {
const user = await getUserById(1);
}
Note: You are using a wrapper function called runProgram
because you cannot use await
in the top level of a file. Doing so would cause the TypeScript Compiler to emit the error 1375
:
Output'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module. (1375)
If you hover over user
in your editor or in the TypeScript Playground, you’ll find that user
has the type User | null
, which is exactly the type the promise returned by your getUserById
function resolves to.
If you remove the await
and just call the function directly, the Promise object is returned:
async function runProgram() {
const userPromise = getUserById(1);
}
If you hover over userPromise
, you’ll find that it has the type Promise<User | null>
.
Most of the time, TypeScript can infer the return type of your async function, just like it does with non-async functions. You can therefore omit the return type of the getUserById
function, as it is still correctly inferred to have the type Promise<User | null>
:
async function getUserById(userId: number) {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
Rest parameters are a feature in JavaScript that allows a function to receive many parameters as a single array. In this section, you will use rest parameters with TypeScript.
Using rest parameters in a type-safe way is completely possible by using the rest parameter followed by the type of the resulting array. Take for example the following code, where you have a function called sum
that accepts a variable amount of numbers and returns their total sum:
function sum(...args: number[]) {
return args.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);
}
This function uses the .reduce
Array method to iterate over the array and add the elements together. Notice the rest parameter args
highlighted here. The type is being set to an array of numbers: number[]
.
Calling your function works normally:
function sum(...args: number[]) {
return args.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);
}
const sumResult = sum(2, 4, 6, 8);
If you call your function using anything other than a number, like:
const sumResult = sum(2, "b", 6, 8);
The TypeScript Compiler will emit the error 2345
:
OutputArgument of type 'string' is not assignable to parameter of type 'number'. (2345)
Programmers sometime need a function to accept different parameters depending on how the function is called. In JavaScript, this is normally done by having a parameter that may assume different types of values, like a string or a number. Setting multiple implementations to the same function name is called function overloading.
With TypeScript, you can create function overloads that explicitly describe the different cases that they address, improving the developer experience by document each implementation of the overloaded function separately. This section will go through how to use function overloading in TypeScript.
Imagine you have a User
type:
type User = {
id: number;
email: string;
fullName: string;
age: number;
};
And you want to create a function that can look up a user using any of the following information:
id
email
age
and fullName
You could create such a function like this:
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
This function uses the |
operator to compose a union of types for idOrEmailOrAge
and for the return value.
Next, add function overloads for each way you want your function to be used, as shown in the following highlighted code:
type User = {
id: number;
email: string;
fullName: string;
age: number;
};
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
This function has three overloads, one for each way to retrieve a user. When creating function overloads, you add the function overloads before the function implementation itself. The function overloads do not have a body; they just have the list of parameters and the return type.
Next, you implement the function itself, which should have a parameter list that is compatible with all function overloads. In the previous example, your first parameter can be either a number or a string, since it can be the id
, the email
, or the age
:
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
You therefore set the type of the idOrEmailorAge
parameter in your function implementation to be number | string
. This way, it is compatible with all the overloads of your getUser
function.
You are also adding an optional parameter to your function, for when the user is passing a fullName
:
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
Implementing your function could be like the following, where you are using a users
array as the data store of your users:
type User = {
id: number;
email: string;
fullName: string;
age: number;
};
const users: User[] = [
{ id: 1, email: "jane_doe@example.com", fullName: "Jane Doe" , age: 35 },
{ id: 2, email: "jon_do@example.com", fullName: "Jon Doe", age: 35 }
];
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
if (typeof idOrEmailOrAge === "string") {
return users.find(user => user.email === idOrEmailOrAge);
}
if (typeof fullName === "string") {
return users.find(user => user.age === idOrEmailOrAge && user.fullName === fullName);
} else {
return users.find(user => user.id === idOrEmailOrAge);
}
}
const userById = getUser(1);
const userByEmail = getUser("jane_doe@example.com");
const userByAgeAndFullName = getUser(35, "Jon Doe");
In this code, if idOrEmailOrAge
is a string, then you can search for the user with the email
key. The following conditional assumes idOrEmailOrAge
is a number, so it is either the id
or the age
, depending on if fullName
is defined.
One interesting aspect of function overloads is that in most editors, including VS Code and the TypeScript Playground, as soon as you type the function name and open the first parenthesis to call the function, a pop-up will appear with all the overloads available, as shown in the following image:
If you add a comment to each function overload, the comment will also be in the pop-up as a source of documentation. For example, add the following highlighted comments to the example overloads:
...
/**
* Get a user by their ID.
*/
function getUser(id: number): User | undefined;
/**
* Get a user by their email.
*/
function getUser(email: string): User | undefined;
/**
* Get a user by their age and full name.
*/
function getUser(age: number, fullName: string): User | undefined;
...
Now when you hover over these functions, the comment will show up for each overload, as shown in the following animation:
The last feature of functions in TypeScript that this tutorial will examine is user-defined type guards, which are special functions that allow TypeScript to better infer the type of some value. These guards enforce certain types in conditional code blocks, where the type of a value may be different depending on the situation. These are especially useful when using the Array.prototype.filter
function to return a filtered array of data.
One common task when adding values conditionally to an array is to check for some conditions and then only add the value if the condition is true. If the value is not true, the code adds a false
Boolean to the array. Before using that array, you can filter it using .filter(Boolean)
to make sure only truthy values are returned.
When called with a value, the Boolean constructor returns true
or false
, depending on if this value is a Truthy
or Falsy
value.
For example, imagine you have an array of strings, and you only want to include the string production
to that array if some other flag is true:
const isProduction = false
const valuesArray = ['some-string', isProduction && 'production']
function processArray(array: string[]) {
// do something with array
}
processArray(valuesArray.filter(Boolean))
While this is, at runtime, perfectly valid code, the TypeScript Compiler will give you the error 2345
during compilation:
OutputArgument of type '(string | boolean)[]' is not assignable to parameter of type 'string[]'.
Type 'string | boolean' is not assignable to type 'string'.
Type 'boolean' is not assignable to type 'string'. (2345)
This error is saying that, at compile-time, the value passed to processArray
is interpreted as an array of false | string
values, which is not what the processArray
expected. It expects an array of strings: string[]
.
This is one case where TypeScript is not smart enough to infer that by using .filter(Boolean)
you are removing all falsy
values from your array. However, there is one way to give this hint to TypeScript: using user-defined type guards.
Create a user-defined type guard function called isString
:
function isString(value: any): value is string {
return typeof value === "string"
}
Notice the return type of the isString
function. The way to create user-defined type guards is by using the following syntax as the return type of a function:
parameterName is Type
Where parameterName
is the name of the parameter you are testing, and Type
is the expected type the value of this parameter has if this function returns true
.
In this case, you are saying that value
is a string
if isString
returns true
. You are also setting the type of your value
parameter to any
, so it works with any
type of value.
Now, change your .filter
call to use your new function instead of passing it the Boolean
constructor:
const isProduction = false
const valuesArray = ['some-string', isProduction && 'production']
function processArray(array: string[]) {
// do something with array
}
function isString(value: any): value is string {
return typeof value === "string"
}
processArray(valuesArray.filter(isString))
Now the TypeScript compiler correctly infers that the array passed to processArray
only contains strings, and your code compiles correctly.
Functions are the building block of applications in TypeScript, and in this tutorial you learned how to build type-safe functions in TypeScript and how to take advantage of function overloads to better document all variants of a single function. Having this knowledge will allow for more type-safe and easy-to-maintain functions throughout your code.
For more tutorials on TypeScript, check out our How To Code in TypeScript series page.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
TypeScript is an extension of the JavaScript language that uses JavaScript’s runtime with a compile-time type checker. This combination allows developers to use the full JavaScript ecosystem and language features, while also adding optional static type-checking, enum data types, classes, and interfaces.
This series will show you the syntax you need to get started with TypeScript, allowing you to leverage its typing system to make scalable, enterprise-grade code.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!