What is TypeScript?
TypeScript is a superset of JavaScript. All JavaScript code is valid TypeScript code.
- It adds types to JavaScript.
- It is a strongly typed language.
- It is compiled to JavaScript.
TypeScript helps us identify errors at compile time through the process of static type checking. This means that the TypeScript compiler will check our code for errors before the code gets executed (without running it).
What is an Interface?
Interface defines the structure of an object. In our example, we have a Person
interface that defines the structure of a person
object.
1
2
3
4
5
6
7
8
9
| interface Person {
name: string;
age: number;
}
let person: Person = {
name: "John",
age: 30,
};
|
What is a Type?
In TypeScript, a type
is a way to refer to the different properties
and functions
that a value
has. In our example, we have a Todo
type that refers to the different properties that a todo object
has.
1
2
3
4
5
| type Todo = {
id: number;
title: string;
completed: boolean;
};
|
Another example is a variable of type string
and with a value of hello
.
1
| const hello: string = "hello";
|
This means that the variable hello
can only have a value of type string
including all the properties and functions that a string
has. For example, the toUpperCase()
function.
1
2
3
| const hello: string = "hello";
hello.toUpperCase();
|
Every value in TypeScript has a type. For example, the value true
has a type of boolean
.
1
| const isDone: boolean = true;
|
What are Type categories?
Types in TypeScript can be divided into two categories:
- Primitive types (number, boolean, void, undefined, string, symbol, null)
- Object types (functions, arrays, classes, objects)
Types are used by the TypeScript compiler to analyze our code for errors. For example, if we try to assign a value of type string
to a variable of type number
, the TypeScript compiler will throw an error.
1
| const age: number = "hello"; // Error: Type '"hello"' is not assignable to type 'number'.
|
What are Type annotations?
These are used to tell TypeScript what type of value a variable will refer to. For example, we have a variable age
that refers to a value of type number
.
- when we declare a variable and add a semicolon followed by a type annotation, we are telling TypeScript that the variable
age
will always be assigned a value of type number
.
1
2
| let age: number;
age = 30;
|
- If later on, we try to assign a value of type
string
to the variable age
, the TypeScript compiler will throw an error.
1
2
3
| let age: number;
age = 30;
age = "hello"; // Error: Type '"hello"' is not assignable to type 'number'.
|
- The following is an example of an array variable with a type annotation.
1
| let names: string[] = ["John", "Jane", "Mary"];
|
This means that the variable names
can only have an array of strings as its value.
- Similarly, we can declare a variable which can only have numbers as its value.
1
| let ages: number[] = [30, 40, 50];
|
Other examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| let hasName: boolean = true;
let nothingMuch: null = null;
let nothing: undefined = undefined;
// built in objects
let now: Date = new Date();
// Array
let colors: string[] = ["red", "green", "blue"];
let myNumbers: number[] = [1, 2, 3, 4, 5];
// Classes
class Car {}
let car: Car = new Car();
// Object literal
let point: { x: number; y: number } = {
x: 10,
y: 20,
};
|
Type annotations for functions
1
2
3
4
| // Function
const logNumber: (i: number) => void = (i: number) => {
console.log(i);
};
|
- In the above example, we have a function
logNumber
that takes in a parameter i
of type number
and returns void
. - We can understand the above function by breaking it down into 3 parts as follows:
const logNumber
is a variable: (i: number) => void
is the type annotation that tells TypeScript what type of value the variable logNumber will refer to.
void
is the return type of the function. In this case, the function does not return anything.
= (i: number) => { console.log(i); }
is the value that the variable logNumber
refers to.
(i: number) => { console.log(i); }
is the function expression that takes in a parameter i
of type number
and returns void
.
When to use type annotations?
- When we declare a variable on one line and initialize it later for example, we need to add a type annotation.
1
2
| let age: number;
age = 30;
|
In the following example if we don’t add a type annotation, the TypeScript compiler will throw an error.
1
2
3
4
5
6
7
8
| let words = ["red", "green", "blue"];
let foundWord;
for (let i = 0; i < words.length; i++) {
if (words[i] === "green") {
foundWord = true;
}
}
|
- In the above example, we have a variable
foundWord
that is declared on one line and initialized later. If we don’t add a type annotation, the TypeScript compiler will throw an error.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| let words = ["red", "green", "blue"];
let foundWord: boolean;
for (let i = 0; i < words.length; i++) {
if (words[i] === "green") {
foundWord = true;
}
}
// OR
let words = ["red", "green", "blue"];
let foundWord = false;
for (let i = 0; i < words.length; i++) {
if (words[i] === "green") {
foundWord = true;
}
}
|
- When we want a variable to have a type that can’t be inferred correctly.
1
| let names = ["John", "Jane", "Mary"];
|
In the following example, we have a variable with an array of numbers and another variable that is supposed to hold the largest number in the array.
1
2
3
4
5
6
7
8
| let numbers = [-10, -1, 12];
let numberAboveZero = false;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > 0) {
numberAboveZero = numbers[i];
}
}
|
- In the above example, we have a variable
numberAboveZero
that is supposed to hold the largest number in the array. If we don’t add a type annotation, the TypeScript compiler will throw an error. To fix this, we can add a type annotation to the numberAboveZero
variable.
1
2
3
4
5
6
7
8
| let numbers = [-10, -1, 12];
let numberAboveZero: boolean | number = false;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > 0) {
numberAboveZero = numbers[i];
}
}
|
- When a function returns the
any
type and we need to clarify the value.
1
2
3
| const json = '{"x": 10, "y": 20}';
const coordinates = JSON.parse(json);
console.log(coordinates); // {x: 10, y: 20};
|
- In the above example, the
coordinates
variable has a type of any
. This is because the JSON.parse()
function can return any type of value. We can add a type annotation to the coordinates
variable to clarify the value. In the following code, we will examine how JSON.parse() works when passed different types of values.
1
2
3
4
5
6
| JSON.parse("false"); // false
JSON.parse("4"); // 4 (number)
JSON.parse('{"x": 10, "y": 20}'); // {x: 10, y: 20} (object)
JSON.parse("John"); // "John" (string)
JSON.parse('{"value": 5}'); // {value: number} (object)
JSON.parse('{"name": "John"}'); // {name: string} (object)
|
- In the above example, we can see that the
JSON.parse()
function can return different types of values. This is why the coordinates
variable has a type of any
. We can add a type annotation to the coordinates
variable to clarify the value.
1
2
3
| const json = '{"x": 10, "y": 20}';
const coordinates: { x: number; y: number } = JSON.parse(json);
console.log(coordinates); // {x: 10, y: 20};
|
What is Type inference?
This is a feature of TypeScript that allows us to omit type annotations. For example, we have a variable age
that refers to a value of type number
.
- We don’t need to add a type annotation because TypeScript can infer the type of the value.
1
| let age = 30; // Type inference
|
Some explanations on type inference
- When we declare a variable and assign a value to it, we are performing a 2 step process.
- first, we declare a variable (e.g.
let age
, or const hello
). This is called variable declaration - then, we assign a value to the variable (e.g.
age = 30
, or hello = "hello"
). This is called variable initialization
- If we declare a variable and assign a value to it on the same line, type annotations are not needed because TypeScript will automatically infer the type of the value for us.
- For example, we have a variable
age
that refers to a value of type number
.
1
| let age = 30; // Type inference
|
- On the other hand, if we declare a variable and assign a value to it on different lines, we need to add a type annotation because TypeScript will not be able to infer the type of the value for us.
- For example, we have a variable
age
that refers to a value of type number
.
1
2
| let age; // Type annotation
age = 30;
|
- In the above example, we have a variable
age
that refers to a value of type number
.- We need to add a type annotation because TypeScript will not be able to infer the type of the value for us.
1
2
| let age: number; // Type annotation
age = 30;
|
When to use type inference?
- It is recommended to
ALWAYS
use type inference when we can.
Type annotations and inference for functions
While we can use type annotations to describe the types of values that variables will refer to, we can also use them to describe the types of values that functions will return.
Type inference works out the return type for us, but we can also add a type annotation to explicitly specify the return type of the function. This is useful when the function returns the any
type. For example, we have a function add
that takes in two parameters a
and b
and returns the sum of the two parameters.
1
2
3
| const add = (a: number, b: number): number => {
return a + b;
};
|
- We can understand the above code as follows:
const add
- declare a variable add
that refers to a value of type function
(a: number, b: number)
- the function takes in two parameters a
and b
that are both of type number
: number
- the function returns a value of type number
=>
- the function returns the sum of the two parameters
- When we intend to not return anything from a function, we can use the
void
type annotation. For example, we have a function logNumber
that takes in a parameter num
and logs it to the console.
1
2
3
| const logNumber = (num: number): void => {
console.log(num);
};
|
- We can use the
never
type annotation when we intend to never return anything from a function. For example, we have a function throwError
that takes in a parameter message
and throws an error with the message.
1
2
3
| const throwError = (message: string): never => {
throw new Error(message);
};
|
But we usually don’t need to use the never
type annotation and can use the void
type annotation instead. For example, we have a function throwError
that takes in a parameter message
and throws an error with the message.
1
2
3
4
5
| const throwError = (message: string): void => {
if (!message) {
throw new Error(message);
}
};
|