We touched on types in the last section. In this section, we'll go through the basic types that are commonly used in TypeScript so that we start to understand what cases we should use in each type. We'll make heavy use of the online TypeScript playground, so be sure to have that ready.
Understanding basic types
Primitive types
Before understanding how we declare variables and functions with types in TypeScript, let's briefly look at primitive types, which are the most basic types. Primitive types are simple values that have no properties. TypeScript shares the following primitive types with JavaScript:
- string: Represents a sequence of Unicode characters
- number: Represents both integers and floating-point numbers
- boolean: Represents a logical true or false
- undefined: Represents a value that hasn't been initialized yet
- null: Represents no value
Type annotations
Types for JavaScript variables are determined at runtime. Types for JavaScript variables can also change at runtime. For example, a variable that holds a number can later be replaced by a string. Usually, this is unwanted behavior and can result in a bug in our app.
TypeScript annotations let us declare variables with specific types when we are writing our code. This allows the TypeScript compiler to check that the code adheres to these types before the code executes at runtime. In short, type annotations allow TypeScript to catch bugs where our code is using the wrong type much earlier than we would if we were writing our code in JavaScript.
TypeScript annotations let us declare variables with types using the :Type syntax.
- Let's browse to the TypeScript playground and enter the following variable declaration into the left-hand pane:
let unitPrice: number;
- The transpiled JavaScript will appear on the right-hand side as follows:
var unitPrice;
- Let's add a second line to our program:
unitPrice = "Table";
Notice that a red line appears under unitPrice, and if you hover over it, you are correctly informed that there is a type error:
- You can also add type annotations to function parameters for the return value using the same :Type syntax. Let's enter the following function into the playground:
function getTotal(unitPrice: number, quantity: number, discount: number): number {
const priceWithoutDiscount = unitPrice * quantity;
const discountAmount = priceWithoutDiscount * discount;
return priceWithoutDiscount - discountAmount;
}
We've declared unitPrice, quantity, and discount parameters, all as numbers. The return type annotation comes after the function's parentheses, which is also a number in the preceding example.
- Let's call our function with an incorrect type for quantity and assign the result to a variable with an incorrect type:
let total: string = getTotal(500, "one", 0.1);
We find that one is underlined in red, highlighting that there is a type error:
- If we then correct one to 1, total should be underlined in red, highlighting that there is a type problem with that:
The TypeScript compiler uses type annotations to check whether values assigned to variables and function parameters are valid for their type.
This strong type checking is something that we don't get in JavaScript, and it is very useful in large code bases because it helps us immediately detect type errors.
Type inference
We have seen how type annotations are really valuable, but they involve a lot of extra typing. Luckily, TypeScript's powerful type inference system means we don't have to provide annotations all the time. We can use type inference when we immediately set a variable value.
Let's look at an example:
- Let's add the following variable assignment in the TypeScript playground:
let flag = false;
- If we hover our mouse over the flag variable, we can see that TypeScript has inferred the type as boolean:
- If we add another line beneath this, to incorrectly set flag to Table, we get a type error:
So, when we declare a variable and immediately set its type, we can use type inference to save a few keystrokes.
Any
What if we declare a variable with no type annotation and no value? What does TypeScript infer as the type? Let's enter the following code in the TypeScript playground and find out:
let flag;
If we hover our mouse over flag, we see it has been given the any type:
So, the TypeScript compiler gives a variable with no type annotation and no immediately assigned value, the any type. The any type is specific to TypeScript; it doesn't exist in JavaScript. It is a way of opting out of type checking on a particular variable. It is commonly used for dynamic content or values from third-party libraries. However, TypeScript's increasingly powerful type system means that we need to use any less often these days.
Void
void is another type that doesn't exist in JavaScript. It is generally used to represent a non-returning function.
Let's look at an example:
- Let's enter the following function into the TypeScript playground:
function logText(text: string): void {
console.log(text);
}
The function simply logs some text into the console and doesn't return anything. So, we've marked the return type as void.
- If we remove the return type annotation and hover over the function name, logText, we'll see that TypeScript has inferred the type to be void:
This saves us a few keystrokes while writing functions that don't return anything.
Never
The never type represents something that would never occur and is typically used to specify unreachable areas of code. Again, this doesn't exist in JavaScript.
Time for an example:
- Type the following code into the TypeScript playground:
function foreverTask(taskName: string): never {
while (true) {
console.log(`Doing ${taskName} over and over again ...`);
}
}
The function invokes an infinite loop and never returns, and so we have given it a type annotation of never. This is different to void because void means it will return, but with no value.
- Let's change the foreverTask function to break out of the loop:
function foreverTask(taskName: string): never {
while (true) {
console.log(`Doing ${taskName} over and over again ...`);
break;
}
}
The TypeScript compiler quite rightly complains:
- Let's now remove the break statement and the never type annotation. If we hover over the foreverTask function name with our mouse, we see that TypeScript has inferred the type to be void, which is not what we want in this example:
The never type is useful in places where the code never returns. However, we will probably need to explicitly define the never type annotation because the TypeScript compiler isn't smart enough yet to infer that.
Enumerations
Enumerations allow us to declare a meaningful set of friendly names that a variable can be set to. We use the enum keyword, followed by the name we want to give to it, followed by the possible values in curly braces.
Here's an example:
- Let's declare an enum for order statuses in the TypeScript playground:
enum OrderStatus {
Paid,
Shipped,
Completed,
Cancelled
}
- If we look at the transpiled JavaScript, we see that it looks very different:
var OrderStatus;
(function (OrderStatus) {
OrderStatus[OrderStatus["Paid"] = 1] = "Paid";
OrderStatus[OrderStatus["Shipped"] = 2] = "Shipped";
OrderStatus[OrderStatus["Completed"] = 3] = "Completed";
OrderStatus[OrderStatus["Cancelled"] = 4] = "Cancelled";
})(OrderStatus || (OrderStatus = {}));
This is because enumerations don't exist in JavaScript, so the TypeScript compiler is transpiling the code into something that does exist.
- Let's declare a status variable, setting the value to the shipped status:
let status = OrderStatus.Shipped;
Notice how we get nice IntelliSense when typing the value:
- By default, the numerical values start from 0 and increment. However, the starting value can be explicitly declared in the enum, as in the following example, where we set Paid to 1:
enum OrderStatus {
Paid = 1,
Shipped,
Completed,
Cancelled
}
- Let's set our status variable to the shipped status and log this to the console:
let status = OrderStatus.Shipped;
console.log(status);
If we run the program, we should see 2 output in the console:
- In addition, all the values can be explicitly declared, as in the following example:
enum OrderStatus {
Paid = 1,
Shipped = 2,
Completed = 3,
Cancelled = 0
}
Enumerations are great for data such as a status that is stored as a specific set of integers but actually has some business meaning. They make our code more readable and less prone to error.
Objects
The object type is shared with JavaScript and represents a non-primitive type. Objects can contain typed properties to hold bits of information.
Let's work through an example:
- Let's enter the following code into the TypeScript playground, which creates an object with several properties of information:
const customer = {
name: "Lamps Ltd",
turnover: 2000134,
active: true
};
If we hover over name, turnover, and active, we'll see that TypeScript has smartly inferred the types to be string, number, and boolean respectively.
- If we hover over the customer variable name, we see something interesting:
- Rather than the type being object, it is a specific type with name, turnover, and active properties. On the next line, let's set the turnover property to some other value:
customer.turnover = 500000;
As we type the turnover property, IntelliSense provides the properties that are available on the object:
- This line of code is perfectly fine, so we don't get any complaints from the compiler. If we set the turnover to a value that has an incorrect type, we'll be warned as we would expect:
- Now let's set a property on customer that doesn't exist yet:
customer.profit = 10000;
We'll see that TypeScript complains:
This makes sense if we think about it. We've declared customer with name, turnover, and active properties, so setting a profit property should cause an error. If we wanted a profit property, we should have declared it in the original declaration.
In summary, the object type is flexible because we get to define any properties we require, but TypeScript will narrow down the type to prevent us incorrectly typing a property name.
Arrays
Arrays are structures that TypeScript inherits from JavaScript. We add type annotations to arrays as usual, but with square brackets at the end to denote that this is an array type.
Let's take a look at an example:
- Let's declare the following array of numbers in the TypeScript playground:
const numbers: number[] = [];
Here, we have initialized the array as empty.
- We can add an item to the array by using the array's push function. Let's add the number 1 to our array:
numbers.push(1);
- If we add an element with an incorrect type, the TypeScript compiler will complain, as we would expect:
- We can use type inference to save a few keystrokes if we declare an array with some initial values. As an example, if we type in the following declaration and hover over the numbers variable, we'll see the type has been inferred as number[].
const numbers = [1, 3, 5];
- We can access an element in an array by using the element number in square brackets. Element numbers start at 0.
Let's take an example:
- Let's log out the number of elements under the numbers variable declaration, as follows:
console.log(numbers[0]);
console.log(numbers[1]);
console.log(numbers[2]);
- Let's now click the Run option on the right-hand side of the TypeScript playground to run our program. A new browser tab should open with a blank page. If we press F12 to open the Developer tools and go to the console section, we'll see 1, 3, and 5 output to the console.
- There are several ways to iterate through elements in an array. One option is to use a for loop, as follows:
for (let i in numbers) {
console.log(numbers[i]);
}
If we run the program, we'll see 1, 3, and 5 output to the console again.
- Arrays also have a useful function for iterating through their elements, called forEach. We can use this function as follows:
numbers.forEach(function (num) {
console.log(num);
});
- forEach calls a nested function for each array element, passing in the array element. If we hover over the num variable, we'll see it has been correctly inferred as a number. We could have put a type annotation here, but we have saved ourselves a few keystrokes:
Arrays are one of the most common types we'll use to structure our data. In the preceding examples, we've only used an array with elements having a number type, but any type can be used for elements, including objects, which in turn have their own properties.