This article aims to help a beginner understand the problems that TypeScript solves and therefore why it may be useful to them. Before we jump in to this, however, it’s important to have an understanding of some absolute fundamentals of TypeScript:
- Typescript isn’t a new language, it’s an enhancement of the JavaScript syntax you already know.
- You don’t execute a Typescript program. When you write a program with Typescript, it is compiled to JavaScript before being run.
- You can write JavaScript in a file with a Typescript extension (
.ts
), then compile it with the TypeScript compiler and it will work. - Typescript can be thought of as a tool we can optionally use when writing JavaScript programs to add static typing instead of the language’s default dynamic typing.
To help you understand what TypeScript is and why it’s useful - let’s look at some common errors you’re likely to come across when you develop with JavaScript.
Common JavaScript scenario 1
Take the following common JavaScript error:
TypeError: Cannot read property 'userId' of undefined
This error has been caused by trying to access a key in an object that doesn’t exist. It’s an extremely common error to experience. In a React app a simple mistake like this will cause a white screen (if not caught by an ErrorBoundary).
Common JavaScript scenario 2
You’ve just started working in a new team and a new codebase and you see the following function call:
const formattedTransations = handleTransactions(transactions);
What is this function doing? What is this transactions
variable it expects to receive? transactions
could be an array of IDs, or an array of objects. If the latter, what should the objects look like?
You could try to find where the transactions
variable is being created, but this might lead to stepping through a number of functions along the way and it’d be easy to get lost. Maybe you’re lucky and there’s some documentation in the form of a comment. What if it’s outdated though?
What does handleTransations
return? What is formattedTransations
going to be?
You’re going to have questions like these all the time in your new codebase.
How does TypeScript help?
TypeScript allows you (amongst other things) to define the type of data that can be stored in variables, passed to functions or returned from functions. Using these type annotations, you massively reduce your ability to incorrectly pass around data in your codebase. Take the following function as an example:
const addNumbers = (numA, numB) => {
return numA + numB;
};
The function above simply takes two numbers and sums them together.
What happens if we call this function like this:
const sum = addNumbers(9, "9");
The result of this will be "99"
. This is because JavaScript converts the 9 to a string when you use the +
sign for concatenation.
To prevent against this, we can write the same function in TypeScript like this:
const addNumbers = (numA: number, numB: number) => {
return numA + numB;
};
The only thing that differentiates this function from standard JavaScript is the :number
after the numA
and numB
parameters. This is a type annotation, which lets us know that these parameters should only ever be of type number
.
Look what happens now if we pass a string to the function:
const addNumbers = (numA: number, numB: number) => {
return numA + numB;
};
const sum = addNumbers(9, "9");
When you compile this code now you’ll see the following error:
error: Argument of type 'string' is not assignable to parameter of type 'number'.
TypeScript is telling us that we tried to pass a string to the addNumbers
function but instead it was expecting to receive a number.
We can fix the incorrect param and this will prevent the buggy value of “99” from being returned.
Back to Scenario 1
TypeScript’s type definitions extend far beyond simple strings or numbers. Using TypeScript we can define types for every piece of data in our codebase, even complex and large objects.
If we have a user object stored in our database we can add a type to it:
type User = {
userId: string;
age: number;
names: {
first: string;
last?: string;
};
};
Now that we have a type for a user, TypeScript will protect us against mistakes that are easy to make:
const userName = user.name;
Results in a compilation error:
Property 'name' does not exist on type 'User'. Did you mean 'names'?
Let’s create a function that returns a user object when passed some data collected from a form:
const createUser = (
userAge: number,
firstname: string,
lastname?: string
): User => {
return {
userId: generateUserId(),
age: userAge,
names: {
first: firstname,
},
};
};
If we call the createUser
function without the correct number of arguments TypeScript will also warn us:
const newUser = createUser(22);
Results in the following error:
Expected 2-3 arguments, but got 1.
An argument for 'firstname' was not provided.
If you’re wondering why TypeScript isn’t also complaining about lastname
not being passed to the function, it’s because the parameter is marked with a ?
which means it is an optional parameter.
Scenario 2
TypeScript has excellent integration with many IDEs, but in particular Visual Studio Code. This integration gives the benefit of inline errors as you write code.
In a TypeScript codebase, you can simply hover over a function and see what arguments it expects and what it will return:
VScode tells us that this function expects an array full of type Transaction
and will return an array of type string
.
If you don’t know what a Transaction
should look like, you can click the type and it’ll bring you straight to the definition in code:
VSCode will also never allow you to access a property or a method that doesn’t exist on a variable. Instead it makes it super easy to always know what data you have available to safely use, without worrying about an undefined
TypeError:
Summary
Using TypeScript gives you much more confidence in the code you write. In big codebases with multiple contributors, it makes it far easier to understand what shape data should be in, and what is happening to that data.
Without TypeScript, making changes to a single file can have ramifications across many other files. If your changes don’t propagate throughout the entire codebase it’s very likely you may have introduced bugs.
Trying to manually find all of the places your changes affect can be an extremely time consuming process without any guarantee that you’ll actually succeed.
Using TypeScript means that you can make your changes and then follow the errors across the entire codebase until you’ve implemented the changes everywhere necessary. This has the effect of making debt cleanup or large refactors that would be simply impossible with JavaScript super easy.