← Back

React.js + Next.js · Module 1. TypeScript Basics

TypeScript from Scratch: Theory, Notes, and Practical Examples

A complete lesson for the first module of the React.js + Next.js course. You will learn the TypeScript theory first, then work through 19 JavaScript → TypeScript comparisons with detailed explanations.

What you will learn

PART I

Theory and complete notes

TypeScript is an extension of JavaScript that adds a type system to the language.

In JavaScript, we can write:

let price = 100;

price = "one hundred";

JavaScript allows this kind of change.

In TypeScript, we can specify that price must be a number:

let price: number = 100;

price = "one hundred"; // ❌ Error

This allows us to find many errors while writing a program, before the code is ever run.


THEORY · 01

1. What Is TypeScript?

TypeScript is a language developed by Microsoft.

It is a superset of JavaScript, which means it extends JavaScript with additional features.

The most important of these is:

static typing.

Example:

let age: number = 35;

TypeScript knows that age is a number.

Therefore, this is valid:

age = 40; // ✅

but this is not:

age = "forty"; // ❌

TypeScript will report an error.

TypeScript Does Not Run Directly in the Browser

We write:

const message: string = "Hello";

but after TypeScript is processed, it becomes JavaScript:

const message = "Hello";

The type annotation:

:string

disappears.

The process of converting TypeScript into JavaScript is called transpilation.

We can remember it like this:

TypeScript
    ↓
transpilation
    ↓
JavaScript
    ↓
browser / Node.js

THEORY · 02

2. Creating a TypeScript Project with Vite

The easiest way to start a project is with Vite.

In the terminal, run:

npm create vite@latest

Then select TypeScript as the project's language.

After creating the project, run:

cd project-name
npm install
npm run dev

The project also contains this file:

tsconfig.json

This is the TypeScript configuration file.

Among other things, it determines how the code is checked and processed.


THEORY · 03

3. Basic Types

string

Text:

let firstName: string = "Anna";

number

Whole numbers and decimal numbers:

let age: number = 32;
let price: number = 19.99;

TypeScript does not have separate integer or float types.

We use:

number

boolean

A logical value:

let isLoggedIn: boolean = true;

Possible values:

true
false

null

The intentional absence of a value:

let selectedUser: null = null;

undefined

A value has not been assigned yet:

let result: undefined;

THEORY · 04

4. Type Inference – TypeScript Often Knows the Type Automatically

We do not always have to write:

let age: number = 32;

We can write:

let age = 32;

TypeScript sees the number and determines the type automatically:

age → number

Similarly:

let name = "Robert";

TypeScript infers:

name → string

and it recognizes:

let isActive = true;

as:

isActive → boolean

Therefore:

let age = 32;

age = "Robert"; // ❌

will also produce an error.

TypeScript has already determined that age is of type number.

Because age is declared with let, TypeScript infers the wider type number, not the literal type 32. By contrast, a primitive declared with const, such as const quantity = 10, is typically inferred as the literal type 10.

When Should You Write a Type Explicitly?

You do not need to add a type to every variable.

This:

const quantity = 10;

is completely valid.

It is especially useful to specify types explicitly when:

  • describing a function,
  • creating a data model,
  • defining an object received from an API,
  • reusing a type in several places,
  • code without the type would be harder to understand.

THEORY · 05

5. Typing Objects

JavaScript:

const product = {
  name: "Desk",
  price: 300
};

TypeScript can use exactly the same code and infer its structure automatically.

However, we can also write the type explicitly:

const product: {
  name: string;
  price: number;
} = {
  name: "Desk",
  price: 300
};

This means:

product
├── name → string
└── price → number

We can no longer do this:

product.price = "expensive"; // ❌

because price must be a number.


THEORY · 06

6. Interface

If the same kind of object appears repeatedly, we do not want to write out the entire structure every time.

Instead of:

const laptop: {
  name: string;
  price: number;
} = {
  name: "Laptop",
  price: 900
};

we create an interface.

interface Product {
  name: string;
  price: number;
}

Now we can write:

const laptop: Product = {
  name: "Laptop",
  price: 900
};

const monitor: Product = {
  name: "Monitor",
  price: 250
};

Product describes the object's structure.

We can read:

const laptop: Product

as:

laptop must be an object that matches the Product interface.


THEORY · 07

7. Optional Properties with `?`

Not every property has to be required.

Example:

interface User {
  name: string;
  age?: number;
}

name is required.

age is optional.

Therefore, both objects are valid:

const user1: User = {
  name: "Adam"
};

and:

const user2: User = {
  name: "Maria",
  age: 28
};

The symbol:

?

means:

this property may exist, but it is not required.


THEORY · 08

8. `readonly`

If a value should not be changed:

interface Account {
  readonly id: number;
  name: string;
}

We create an object:

const account: Account = {
  id: 101,
  name: "Mark"
};

We can change:

account.name = "John"; // ✅

but not:

account.id = 500; // ❌

id is read-only.

TypeScript enforces readonly during type checking; it does not freeze the object at runtime. It is also shallow, so nested objects must be made read-only separately when needed.


THEORY · 09

9. Arrays

An array of numbers:

const numbers: number[] = [10, 20, 30];

An array of strings:

const cities: string[] = ["Berlin", "Paris", "Warsaw"];

There is also another syntax:

const numbers: Array<number> = [10, 20, 30];

and:

const cities: Array<string> = ["Berlin", "Paris"];

Both forms mean the same thing.

The shorter form is used most often:

number[]
string[]
User[]
Product[]

THEORY · 10

10. Arrays of Objects

Suppose we have:

interface Product {
  id: number;
  name: string;
}

An array of products:

const products: Product[] = [
  {
    id: 1,
    name: "Chair"
  },
  {
    id: 2,
    name: "Table"
  }
];

Each element must match the Product structure.

This is invalid:

products.push({
  id: 3
}); // ❌ name is missing

THEORY · 11

11. Union Type `|`

A union means:

OR.

Example:

let userId: string | number;

We read this as:

userId can be either a string OR a number.

Therefore:

userId = 15;       // ✅
userId = "user15"; // ✅
userId = true;     // ❌

We can remember the symbol:

|

as:

OR

THEORY · 12

12. Literal Types

Instead of allowing any string, we can allow only specific strings.

let theme: "light" | "dark";

Valid values:

theme = "light";
theme = "dark";

Invalid value:

theme = "blue"; // ❌

A very practical example:

type PaymentStatus = "waiting" | "paid" | "failed";

Then:

let payment: PaymentStatus = "waiting";

payment = "paid"; // ✅
payment = "hello"; // ❌

THEORY · 13

13. Custom Types with `type`

We can give a type a name.

Instead of repeatedly writing:

string | number

we create:

type Id = string | number;

Now:

let customerId: Id = 100;
let orderId: Id = "ORDER-200";

Both values are valid.


THEORY · 14

14. Intersection `&`

An intersection means:

AND.

If we have:

interface Person {
  name: string;
}

and:

interface Employee {
  department: string;
}

we can combine them:

type Worker = Person & Employee;

Now Worker must have the properties from both structures:

const worker: Worker = {
  name: "Thomas",
  department: "IT"
};

In simple terms:

Person
  name

+

Employee
  department

=

Worker
  name
  department

Remember:

| = OR
& = AND / COMBINED

THEORY · 15

15. `type` or `interface`?

To begin with, you can follow a simple rule:

interface

Use it most often to describe objects:

interface User {
  id: number;
  name: string;
}

type

It is very convenient for unions, literal types, and intersections:

type Id = string | number;

type Role = "admin" | "user";

type Employee = Person & Job;

In practice, the capabilities of type and interface overlap to some extent, but the rule above is a good starting point.


THEORY · 16

16. Typing Functions

In JavaScript:

function add(a, b) {
  return a + b;
}

In TypeScript:

function add(a: number, b: number): number {
  return a + b;
}

Let us break it down:

a: number

The first parameter must be a number.

b: number

The second parameter must be a number.

): number

The function returns a number.

We can read:

function add(a: number, b: number): number

as:

the function accepts two numbers and returns a number.


THEORY · 17

17. `void`

If a function does not return anything:

function showMessage(message: string): void {
  console.log(message);
}

void means:

the function performs an action but does not return a value.


THEORY · 18

18. Optional Function Parameters

We can use ?.

function greet(name: string, age?: number): void {
  if (age !== undefined) {
    console.log(name + " is " + age + " years old.");
  } else {
    console.log(name);
  }
}

Both calls are valid:

greet("Anna", 25);
greet("Robert");

But this is not:

greet("Anna", "25"); // ❌

age may be omitted, but if it is provided, it must be a number.


THEORY · 19

19. A Function That Works with an Array of Objects

We have:

interface User {
  id: number;
  name: string;
}

We want to get an array of names.

function getNames(users: User[]): string[] {
  return users.map(function (user) {
    return user.name;
  });
}

Data:

const users: User[] = [
  { id: 1, name: "Anna" },
  { id: 2, name: "Mark" },
  { id: 3, name: "Peter" }
];

Call the function:

const names = getNames(users);

names will be:

["Anna", "Mark", "Peter"]

The function:

accepts → User[]
returns → string[]

THEORY · 20

20. Function Types

We can also describe the structure of a function itself.

type MathFunction = (a: number, b: number) => number;

NOTE:

=>

Here, it does not mean that we have to write an arrow function.

It is simply the syntax used to describe a function type.

We can still use an ordinary function:

const multiply: MathFunction = function (a, b) {
  return a * b;
};

MathFunction means:

a function that accepts two numbers and returns a number.


THEORY · 21

21. Enum

An enum lets us create a named set of constant values.

An order status is a good example:

enum OrderStatus {
  New = "NEW",
  Paid = "PAID",
  Sent = "SENT",
  Delivered = "DELIVERED"
}

We can now write:

let status: OrderStatus = OrderStatus.New;

Later:

status = OrderStatus.Paid;

and:

status = OrderStatus.Sent;

Instead of manually entering this string everywhere:

"SENT"

we use:

OrderStatus.Sent

THEORY · 22

22. Numeric Enums

An enum can also contain numbers:

enum HttpStatus {
  Ok = 200,
  NotFound = 404,
  ServerError = 500
}

Example:

console.log(HttpStatus.NotFound);

Output:

404

If we do not provide explicit values:

enum Step {
  Start,
  Processing,
  Finished
}

TypeScript assigns these values by default:

Start      = 0
Processing = 1
Finished   = 2

THEORY · 23

23. Enum or Union Type?

Union type:

type Status = "loading" | "success" | "error";

Usage:

let status: Status = "loading";

Enum:

enum Status {
  Loading = "loading",
  Success = "success",
  Error = "error"
}

Usage:

let status: Status = Status.Loading;

In many simple cases, a union type is sufficient and more concise.

A useful rule of thumb is:

If I do not need the features provided by enum, I start with a union type.


THEORY · 24

24. Generics

A generic can be understood as:

a placeholder for a type.

It is most commonly written as:

<T>

T does not represent one specific type.

The type will be determined later.

Example:

function returnValue<T>(value: T): T {
  return value;
}

This call:

returnValue<string>("Hello");

means:

T = string

And this call:

returnValue<number>(50);

means:

T = number

The same function works with different types.


THEORY · 25

25. Why Use a Generic Instead of `any`?

We could write:

function returnValue(value: any): any {
  return value;
}

The problem is that TypeScript loses information about the type.

A generic version:

function returnValue<T>(value: T): T {
  return value;
}

preserves that type information.

If we pass:

const result = returnValue(50);

TypeScript knows:

result → number

This is why generics provide:

flexibility + type safety.


THEORY · 26

26. A Generic with an Array

A function that returns the first element:

function getFirst<T>(items: T[]): T | undefined {
  return items[0];
}

For:

getFirst([10, 20, 30]);

TypeScript infers:

T = number

For:

getFirst(["red", "green", "blue"]);

TypeScript infers:

T = string

The return type includes:

T | undefined

because an empty array does not have a first element.


THEORY · 27

27. A Generic in an `interface`

We can create a reusable box:

interface Box<T> {
  value: T;
}

A box containing a number:

const priceBox: Box<number> = {
  value: 100
};

Here:

T = number

A box containing text:

const messageBox: Box<string> = {
  value: "Hello"
};

Here:

T = string

In simple terms:

Box<number>

= a box containing a number

Box<string>

= a box containing text.


THEORY · 28

28. A Generic in an API Response

Here is a very practical use case:

interface ApiResponse<T> {
  data: T;
  status: number;
}

A response containing a user:

interface User {
  id: number;
  name: string;
}
const response: ApiResponse<User> = {
  data: {
    id: 1,
    name: "Anna"
  },
  status: 200
};

Here:

T = User

which means:

data: User

We can reuse the same interface for a product:

interface Product {
  id: number;
  title: string;
}
const productResponse: ApiResponse<Product> = {
  data: {
    id: 50,
    title: "Phone"
  },
  status: 200
};

Now:

T = Product

We do not need to create a separate response interface for every data type.


THEORY · 29

29. Promise

A Promise represents the eventual completion or failure of an asynchronous operation.

You can think of it like this:

call a function
      ↓
receive a Promise
      ↓
wait
      ↓
receive the result

The simplest JavaScript version:

function getMessage() {
  return new Promise(function (resolve) {
    resolve("Hello");
  });
}

TypeScript:

function getMessage(): Promise<string> {
  return new Promise(function (resolve) {
    resolve("Hello");
  });
}

The type:

Promise<string>

means:

When the Promise is fulfilled, it will provide a string.

Promise<string> describes the fulfilled value only. The Promise can still be rejected.


THEORY · 30

30. A Promise with a Number

function getPrice(): Promise<number> {
  return new Promise(function (resolve) {
    resolve(250);
  });
}

Read this as:

Promise<number>

= if the Promise is fulfilled, its value will be a number.


THEORY · 31

31. A Promise with an Object

We have:

interface User {
  id: number;
  name: string;
}

The function:

function getUser(): Promise<User> {
  return new Promise(function (resolve) {
    resolve({
      id: 10,
      name: "Adam"
    });
  });
}

Read this as:

Promise<User>

= if the Promise is fulfilled, its value will be one User object.


THEORY · 32

32. A Promise with an Array of Objects

function getUsers(): Promise<User[]> {
  return new Promise(function (resolve) {
    resolve([
      {
        id: 10,
        name: "Adam"
      },
      {
        id: 11,
        name: "Maria"
      }
    ]);
  });
}

Read the types like this:

User

= one user

User[]

= an array of users

Promise<User[]>

= if the Promise is fulfilled, its value will be an array of users.


THEORY · 33

33. `.then()`

We can receive the result of a Promise with .then():

getUser().then(function (user) {
  console.log(user.name);
});

Because getUser() returns:

Promise<User>

TypeScript knows that:

user

has the type:

User

THEORY · 34

34. `async` and `await`

async/await is a more modern way to work with Promises.

Example:

async function getName(): Promise<string> {
  return "Anna";
}

An async function always returns a Promise.

Therefore:

async function getName(): Promise<string>

means:

an asynchronous function that returns a Promise whose fulfilled value is a string.


THEORY · 35

35. Typing `fetch`

Suppose an API returns:

{
  "id": 15,
  "name": "Martin",
  "email": "martin@example.com"
}

We describe the data:

interface User {
  id: number;
  name: string;
  email: string;
}

Then:

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(
    `https://api.example.com/users/${id}`
  );

  const data = (await response.json()) as User;

  return data;
}

Let us break it down.

Parameter

id: number

The function accepts a number as its parameter.

Result

Promise<User>

The function returns a Promise whose fulfilled value is a user.

Fetch

await fetch(...)

We wait for the server response.

JSON

await response.json()

We read the JSON data.

as User

as User

We tell TypeScript:

Treat this value as if it had the User structure.

This is a compile-time assertion, not a runtime check.


THEORY · 36

36. Important: `as User` Does Not Validate Server Data

This is very important.

const data = (await response.json()) as User;

does not mean:

TypeScript checked the server response and confirmed that the data was valid.

It means:

The programmer is telling TypeScript to treat this data as a User.

The assertion does not convert the value or add any missing properties.

If the server returns invalid data, as User will not validate it.

To genuinely check external data, we need validation that runs while the program is executing.


THEORY · 37

37. Fetch Returning an Array

If the API returns multiple users:

async function fetchUsers(): Promise<User[]> {
  const response = await fetch(
    "https://api.example.com/users"
  );

  const data = (await response.json()) as User[];

  return data;
}

Here:

Promise<User[]>

= the function returns a Promise whose fulfilled value is an array of users.

And:

as User[]

= TypeScript should treat the received data as an array of User objects. It does not validate the array at runtime.


THEORY · 38

38. Axios

Axios lets us conveniently describe the response data type with a generic.

Installation:

npm install axios

Import:

import axios from "axios";

Once again, suppose we have:

interface User {
  id: number;
  name: string;
  email: string;
}

We can write:

async function fetchUser(id: number): Promise<User> {
  const response = await axios.get<User>(
    `https://api.example.com/users/${id}`
  );

  return response.data;
}

The key part is:

axios.get<User>()

This uses a generic.

We can think of it as:

T = User

Therefore, TypeScript treats:

response.data

as the type:

User

The generic provides compile-time type information, but it does not validate the server response at runtime.


THEORY · 39

39. Axios with an Array

If the API returns an array of users:

async function fetchUsers(): Promise<User[]> {
  const response = await axios.get<User[]>(
    "https://api.example.com/users"
  );

  return response.data;
}

The key part is:

axios.get<User[]>()

It means:

Treat the response data as an array of User objects at compile time.

It does not check the response data at runtime.


THEORY · 40

40. Fetch vs Axios

Fetch

async function getProduct(): Promise<Product> {
  const response = await fetch("/product");

  const data = (await response.json()) as Product;

  return data;
}

Axios

async function getProduct(): Promise<Product> {
  const response = await axios.get<Product>("/product");

  return response.data;
}

With fetch, we often explicitly assert the type after reading the JSON:

as Product

With Axios, we pass the type as a generic:

axios.get<Product>()

Neither approach validates the received data at runtime. Both only provide type information to TypeScript.


THEORY · 41

41. Key Things to Remember

Basic Types

string
number
boolean
null
undefined

Array

string[]
number[]
User[]

Interface

interface User {
  id: number;
  name: string;
}

Optional Property

age?: number;

Read-Only Property

readonly id: number;

Union

string | number

which means:

string OR number

Intersection

TypeA & TypeB

which means:

TypeA AND TypeB combined

Literal Type

type Role = "admin" | "user";

Function

function add(a: number, b: number): number {
  return a + b;
}

Function with No Return Value

function showMessage(message: string): void {
  console.log(message);
}

Generic

<T>

= a placeholder for a type.

One User

User

An Array of Users

User[]

A Promise with a User

Promise<User>

A Promise with an Array of Users

Promise<User[]>

Axios – One User

axios.get<User>()

Axios – An Array of Users

axios.get<User[]>()

THEORY · 42

42. Quick Reference: How to Read TypeScript

let age: number;

age is a number.

let name: string;

name is a string.

const users: User[] = [];

users is an array of users.

function getUser(): User

The function returns a user.

function getUsers(): User[]

The function returns an array of users.

function getUser(): Promise<User>

The function returns a Promise whose fulfilled value is a user.

function getUsers(): Promise<User[]>

The function returns a Promise whose fulfilled value is an array of users.

axios.get<User>()

Axios treats the response data as one user at compile time.

axios.get<User[]>()

Axios treats the response data as an array of users at compile time.

interface Box<T>

Box can work with different types.

Box<number>

T = number.

Box<string>

T = string.


THEORY · 43

Summary

TypeScript does not replace JavaScript with a different programming model.

In most cases, we still write ordinary JavaScript:

function add(a, b) {
  return a + b;
}

and add type information:

function add(a: number, b: number): number {
  return a + b;
}

The central idea of TypeScript is:

The more TypeScript knows about our data, the earlier it can detect a mistake.

At the beginning, the most important concepts to understand are:

string
number
boolean

interface

T[]

Union |

Intersection &

function → parameters + return type

<T> → Generic

Promise<T>

axios.get<T>()

Once these concepts are clear, you are ready to start using TypeScript with React.

PART II

Practice: JavaScript → TypeScript

You already know the theory. Now you will add data information to familiar JavaScript functions. On a wide screen, JavaScript appears on the left and TypeScript on the right. On a phone, the examples are stacked.

EXAMPLE 01 / 19

A variable: a value is not the same as a type

First, look at three ordinary values. TypeScript adds information about which values should be accepted by the variables.

JavaScript

.js file
let price = 80;
let productName = "Shelf";
let available = true;

price = 95;
console.log(price);

TypeScript

.ts file
let price: number = 80;
let productName: string = "Shelf";
let available: boolean = true;

price = 95;
console.log(price);

Output from both versions

95

What are we storing?

80 is a value. number is the name of a type. Similarly, "Shelf" is text, while string describes the text type. boolean means either true or false.

What did we add?

We added a colon and a type after the variable name: price: number. This does not change how assignment works.

Do you always have to write the type?

No. In a TypeScript file, let price = 80; also allows TypeScript to determine the number type. This is type inference. Without an annotation, const size = 18 can have the narrower literal type 18; let size = 18 usually has the type number.

Where does the checking end?

Annotations help check your code. They do not turn JavaScript into a language that automatically blocks every incompatible value at runtime. A value in JavaScript has a type; a variable can later refer to a value of a different type.

Intentional error — check it in TypeScript

price = "eighty"; // TS error: a string was used instead of a number.

Documentation for this example: TypeScript — Everyday Types

EXAMPLE 02 / 19

A simple function that returns a number

We want to add the delivery cost to the cost of the items. We begin with an untyped function and then add the types.

JavaScript

.js file
function addDelivery(itemsCost, deliveryCost) {
  return itemsCost + deliveryCost;
}

const total = addDelivery(80, 15);
console.log(total);

TypeScript

.ts file
function addDelivery(
  itemsCost: number,
  deliveryCost: number
): number {
  return itemsCost + deliveryCost;
}

const total = addDelivery(80, 15);
console.log(total);

Output from both versions

95

What goes into the function?

itemsCost receives 80, and deliveryCost receives 15. These are two parameters; we pass arguments to them when we call the function.

What does return do?

return itemsCost + deliveryCost; sends the result of the addition back to the place where the function was called. The total variable receives 95.

What exactly does TypeScript add?

itemsCost: number and deliveryCost: number describe the inputs. ): number, written after the parameter list, describes the result. Read the whole signature as “two numbers in, one number out.”

Why is this check useful?

In JavaScript, addDelivery("80", 15) produces the string "8015", because adding a string performs concatenation. TypeScript reports the incompatible argument before you run the code. This is a warning during type checking, not automatic validation of data received from the network.

Intentional error — check it in TypeScript

addDelivery("80", 15); // TS error: the first argument must be a number.

Documentation for this example: TypeScript — Everyday Types

EXAMPLE 03 / 19

The result can be a string or a boolean

Not every function performs a calculation. One function can prepare a label, while another answers whether a product is available.

JavaScript

.js file
function makeLabel(name) {
  return "Product: " + name;
}

function isAvailable(quantity) {
  return quantity > 0;
}

console.log(makeLabel("Coat rack"));
console.log(isAvailable(6));
console.log(isAvailable(0));

TypeScript

.ts file
function makeLabel(name: string): string {
  return "Product: " + name;
}

function isAvailable(quantity: number): boolean {
  return quantity > 0;
}

console.log(makeLabel("Coat rack"));
console.log(isAvailable(6));
console.log(isAvailable(0));

Output from both versions

Product: Coat rack
true
false

The first function

makeLabel accepts a string and returns a string. Both types are string, but the first describes the parameter and the second describes the result.

The second function

quantity > 0 is a comparison. Its result is true or false, so we write boolean after the parameter list, not number.

An important distinction

The input type and return type do not have to be the same. A function can accept a number and return a string, an object, or a boolean—it depends on the function's purpose.

Documentation for this example: TypeScript — Everyday Types

EXAMPLE 04 / 19

console.log and return: two different jobs

This function only displays a message. It does not prepare a result for use in further calculations.

JavaScript

.js file
function showNotice(message) {
  console.log(message);
}

const result = showNotice("Product saved");
console.log(result);

TypeScript

.ts file
function showNotice(message: string): void {
  console.log(message);
}

const result = showNotice("Product saved");
console.log(result);

Output from both versions

Product saved
undefined

What do you see in the console?

The first message comes from inside the function. The second line reads the value received by the result variable.

Why does undefined appear?

console.log displays something, but it does not replace return. In JavaScript, an ordinary function that finishes without returning a value produces undefined.

What does void mean?

Here, void says that the function does not provide a useful result. It does not mean zero, an empty string, or that the function does nothing. In TypeScript, the result variable has the type void.

Documentation for this example: TypeScript — Functions

EXAMPLE 05 / 19

An optional parameter: you may omit it

A package description must always contain a name. We add the weight only when it is provided.

JavaScript

.js file
function describePackage(name, weight) {
  if (weight === undefined) {
    return name;
  }

  return name + ": " + weight + " kg";
}

console.log(describePackage("Package A", 7));
console.log(describePackage("Package B"));

TypeScript

.ts file
function describePackage(
  name: string,
  weight?: number
): string {
  if (weight === undefined) {
    return name;
  }

  return name + ": " + weight + " kg";
}

console.log(describePackage("Package A", 7));
console.log(describePackage("Package B"));

Output from both versions

Package A: 7 kg
Package B

What does the question mark mean?

weight?: number lets you omit the second argument. You may also pass undefined. When you provide a specific weight, a number is the correct type.

Why do we need the check?

When the argument is missing, weight has the value undefined. The first branch returns only the name. In the rest of the function, TypeScript now knows that we are working with a number.

Why not use if (weight)?

That condition would also treat 0 as false. Comparing with undefined specifically checks for a missing value, not whether the value is “truthy.”

The connection to objects

In an interface, weight?: number also describes an optional property. Reading a property that is not present can produce undefined.

Intentional error — check it in TypeScript

describePackage("Package C", "7"); // TS error: string was used instead of number.

Documentation for this example: TypeScript — Functions

EXAMPLE 06 / 19

A function that returns an object: interface Product

The function will create an object that describes a product. An interface lets us give the expected structure a name.

JavaScript

.js file
function createProduct() {
  return {
    id: 17,
    name: "Shelf",
    price: 80
  };
}

const product = createProduct();
console.log(product.name);

TypeScript

.ts file
interface Product {
  id: number;
  name: string;
  price: number;
}

function createProduct(): Product {
  return {
    id: 17,
    name: "Shelf",
    price: 80
  };
}

const product = createProduct();
console.log(product.name);

Output from both versions

Shelf

What does the interface describe?

Product gives the structure a name: id is a number, name is a string, and price is a number. The interface itself does not create a product.

Where is the object created?

Only the return { ... }; statement creates and returns the data. : Product describes its expected structure while the code is being checked.

What can TypeScript catch?

For example, it can catch a missing required name, a string in place of the price, or an attempt to read a property that the type does not describe. A type describes requirements; it is not a filter that automatically removes extra data received from a server.

Two useful forms

description?: string would mean an optional description. readonly id: number would prevent assignment to id through a reference typed this way. readonly does not freeze the object at runtime.

Intentional error — check it in TypeScript

product.price = "eighty"; // TS error.

console.log(product.email); // TS error: this type has no email property.

Documentation for this example: TypeScript — Object Types

EXAMPLE 07 / 19

A function with an array of products and an ordinary callback

We have a list of products. We want to receive only their names, still as an array.

JavaScript

.js file
function getProductNames(products) {
  return products.map(function (product) {
    return product.name;
  });
}

const products = [
  { id: 17, name: "Shelf", price: 80 },
  { id: 18, name: "Coat rack", price: 25 }
];

const names = getProductNames(products);
console.log(names.join(", "));

TypeScript

.ts file
interface Product {
  id: number;
  name: string;
  price: number;
}

function getProductNames(products: Product[]): string[] {
  return products.map(function (product) {
    return product.name;
  });
}

const products: Product[] = [
  { id: 17, name: "Shelf", price: 80 },
  { id: 18, name: "Coat rack", price: 25 }
];

const names = getProductNames(products);
console.log(names.join(", "));

Output from both versions

Shelf, Coat rack

How should you read the square brackets?

Product is one product. Product[] is an array of products. string[] is an array of strings. Array<Product> means the same array type as Product[].

Why are there two return statements?

The inner return product.name passes the name of one product to map. The outer return products.map(...) returns the complete new array from getProductNames.

What is function (product)?

It is an ordinary anonymous function passed as a callback. map calls it for each array element. We are not using an arrow function.

Why does the callback parameter have no type annotation?

From Product[], TypeScript infers that product has the type Product. There is no need to repeat that information.

Important

The array can be empty. The Product[] type alone does not guarantee that products[0] exists. This example does not read an element outside the array's bounds.

Documentation for this example: TypeScript — Everyday Types · TypeScript — Object Types

EXAMPLE 08 / 19

Unions and literals: choose one of the permitted values

Delivery has two options. The function accepts the name of an option and creates a description.

JavaScript

.js file
function describeDelivery(method) {
  if (method === "pickup") {
    return "Store pickup";
  }

  return "Courier delivery";
}

console.log(describeDelivery("pickup"));
console.log(describeDelivery("courier"));

TypeScript

.ts file
type DeliveryMethod = "pickup" | "courier";

function describeDelivery(method: DeliveryMethod): string {
  if (method === "pickup") {
    return "Store pickup";
  }

  return "Courier delivery";
}

console.log(describeDelivery("pickup"));
console.log(describeDelivery("courier"));

Output from both versions

Store pickup
Courier delivery

What does type mean?

We give a type description a name. Here, DeliveryMethod means exactly "pickup" | "courier".

What does the vertical bar mean?

| means “or.” We allow the string "pickup" or the string "courier", not just any string. These specific strings are literal types.

What about several kinds of data?

The declaration type ProductId = string | number; accepts either a string or a number. Before calling a method that exists only on strings, you must narrow this type—for example, with the condition typeof id === "string".

What does this not do?

The type does not automatically check a form or a server response. If an invalid value reaches JavaScript despite the typing, the function's ordinary logic will run.

Intentional error — check it in TypeScript

describeDelivery("bicycle"); // TS error: this option is not permitted.

Documentation for this example: TypeScript — Everyday Types

EXAMPLE 09 / 19

Intersection: an object must satisfy two descriptions at once

One type describes a product name, while another describes the number of units. The function needs both pieces of information.

JavaScript

.js file
function describeStock(item) {
  return item.name + ": " + item.quantity + " units";
}

const shelf = {
  name: "Shelf",
  quantity: 6
};

console.log(describeStock(shelf));

TypeScript

.ts file
interface NamedProduct {
  name: string;
}

interface Stock {
  quantity: number;
}

type StockProduct = NamedProduct & Stock;

function describeStock(item: StockProduct): string {
  return item.name + ": " + item.quantity + " units";
}

const shelf: StockProduct = {
  name: "Shelf",
  quantity: 6
};

console.log(describeStock(shelf));

Output from both versions

Shelf: 6 units

What does & mean?

NamedProduct & Stock means that a value satisfies the requirements of NamedProduct and the requirements of Stock. In this example, both name and quantity are required.

Does & combine two objects at runtime?

No. It is an operation on types. We create the data separately in const shelf = { ... }. The type will not add a missing property.

How is it different from a union?

A | B: the value matches at least one of the alternatives. A & B: the value satisfies both at once. A union of object types is not always an exclusive “either/or”: an object can also match both alternatives.

What happens when the requirements conflict?

If both types require the same property but one expects string and the other expects number, simply “combining the fields” is not enough. The requirements must be possible to satisfy at the same time.

Intentional error — check it in TypeScript

describeStock({ name: "Shelf" }); // TS error: quantity is missing.

Documentation for this example: TypeScript — Object Types

EXAMPLE 10 / 19

Enum: named values used by a function

The code should use named delivery states. In JavaScript, we show a similar set in an object; in TypeScript, we use an enum.

JavaScript

.js file
const DeliveryState = {
  Prepared: "PREPARED",
  Sent: "SENT",
  Received: "RECEIVED"
};

function showDeliveryState(state) {
  return "Status: " + state;
}

console.log(showDeliveryState(DeliveryState.Sent));

TypeScript

.ts file
enum DeliveryState {
  Prepared = "PREPARED",
  Sent = "SENT",
  Received = "RECEIVED"
}

function showDeliveryState(state: DeliveryState): string {
  return "Status: " + state;
}

console.log(showDeliveryState(DeliveryState.Sent));

Output from both versions

Status: SENT

Which value are we reading?

DeliveryState.Sent produces the string "SENT". The name Sent and its assigned value are two different things.

What does an enum add?

We get named members and a type that we can use for the function parameter. The ordinary object on the left is a behavioural comparison for learning purposes—it is not an identical counterpart to every enum rule.

Numbers are possible too

Enum members can have string or numeric values. In a simple numeric enum without initialisers, successive values begin at 0. Enum members are not arbitrary objects or boolean values.

Enum or union?

When you only need a set of permitted strings, a union is often enough. Unlike an interface or type alias, an ordinary enum usually generates an object in the JavaScript output. The size of a project does not determine whether you should use an enum.

Important

This example uses an ordinary enum. The erasableSyntaxOnly setting does not permit this syntax. Do not change the configuration of an entire project merely to render an example as text.

Documentation for this example: TypeScript — Enums

EXAMPLE 11 / 19

A function type does not require an arrow function

We want to name this requirement: a function accepts two numbers and returns a number. We still implement it with function.

JavaScript

.js file
function multiplyPrice(price, quantity) {
  return price * quantity;
}

const operation = multiplyPrice;
console.log(operation(25, 4));

TypeScript

.ts file
type PriceOperation = (
  price: number,
  quantity: number
) => number;

function multiplyPrice(
  price: number,
  quantity: number
): number {
  return price * quantity;
}

const operation: PriceOperation = multiplyPrice;
console.log(operation(25, 4));

Output from both versions

100

What does type PriceOperation do?

It describes how a function is called: two numeric arguments and a numeric result. It does not perform any multiplication.

Why is => used here?

In the type declaration, => number describes the result type. It is not an arrow-function implementation. The actual work is performed by the ordinary function declaration function multiplyPrice.

What do we assign to operation?

We assign the multiplyPrice function itself, without parentheses. The parentheses appear later: operation(25, 4) calls the function. TypeScript checks whether the assigned function is compatible with the expected type.

Documentation for this example: TypeScript — Functions

EXAMPLE 12 / 19

Generics in a function: a type connected to an argument

This function returns the last element of an array. It should work with both numbers and strings.

JavaScript

.js file
function getLast(items) {
  return items[items.length - 1];
}

const size = getLast([18, 32, 46]);
const wood = getLast(["Ash", "Birch"]);

console.log(size);
console.log(wood);
console.log(getLast([]));

TypeScript

.ts file
function getLast<T>(items: T[]): T | undefined {
  return items[items.length - 1];
}

const size = getLast([18, 32, 46]);
const wood = getLast(["Ash", "Birch"]);

console.log(size);
console.log(wood);
console.log(getLast<number>([]));

Output from both versions

46
Birch
undefined

What does <T> mean?

T is a type parameter. T[] means an array whose elements have that type. For an array of numbers, TypeScript determines that T = number; for an array of strings, T = string.

Why is the result T | undefined?

An empty array has no last element. Therefore, size has the type number | undefined and wood has the type string | undefined, even though these particular calls return a value.

Do you always have to write <number>?

No. TypeScript usually infers the type from the argument. An explicit type can be useful with an empty array because there are no elements from which TypeScript can infer the intended type.

Why not use any?

any disables important checks on how the value is used. A generic preserves the relationship between the input data type and the result. A generic function does not have to return the same value or always return T; here, the result is T | undefined.

Important

Reading an array element by index can produce undefined. The noUncheckedIndexedAccess option lets TypeScript account for this problem more broadly; the strict option alone does not enable it.

Documentation for this example: TypeScript — Generics · TypeScript — noUncheckedIndexedAccess

EXAMPLE 13 / 19

Generics in a structure: one box, different types

We will create a function that packs a value into an object. The content property can hold different kinds of data.

JavaScript

.js file
function packValue(value) {
  return {
    content: value
  };
}

const numberBox = packValue(46);
const textBox = packValue("Ash");

console.log(numberBox.content);
console.log(textBox.content);

TypeScript

.ts file
interface StorageBox<T> {
  content: T;
}

function packValue<T>(value: T): StorageBox<T> {
  return {
    content: value
  };
}

const numberBox = packValue<number>(46);
const textBox = packValue<string>("Ash");

console.log(numberBox.content);
console.log(textBox.content);

Output from both versions

46
Ash

What does the interface do?

StorageBox<T> describes an object with a content property of type T. We do not need to create separate NumberBox and TextBox interfaces.

How should you read the function?

packValue<T> accepts a value of type T and returns StorageBox<T>. The input type and result type are not identical, but they are connected.

Two specific uses

packValue<number>(46) produces an object whose content is a number. packValue<string>("Ash") produces an object whose content is a string. We show explicit type arguments here for learning purposes.

The same idea in an API response

An ApiResult<T> interface can contain data: T and status: number. ApiResult<Product> describes one product inside data, while ApiResult<Product[]> describes an array. Such an interface does not send a request or determine the server's response format by itself.

Documentation for this example: TypeScript — Generics

EXAMPLE 14 / 19

A Promise with a string: receive the result in .then()

We will simulate waiting for a message. This example works without a server. All callbacks are ordinary functions.

JavaScript

.js file
function loadNotice() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve("Warehouse ready");
    }, 500);
  });
}

loadNotice().then(function (message) {
  console.log(message);
});

TypeScript

.ts file
function loadNotice(): Promise<string> {
  return new Promise<string>(function (resolve) {
    setTimeout(function () {
      resolve("Warehouse ready");
    }, 500);
  });
}

loadNotice().then(function (message) {
  console.log(message);
});

Output from both versions

Warehouse ready

What does loadNotice return?

It immediately returns a Promise object, not a string. The function passed to the new Promise constructor runs immediately and schedules a timer.

Why use setTimeout?

The timer callback is scheduled to run after approximately 500 ms. This is not a guarantee of the exact execution time; other work in the program can delay it.

What does resolve do?

resolve("Warehouse ready") fulfils this Promise with a string. It does not replace the outer return: return new Promise(...) is what returns the Promise from the function.

How should you read Promise<string>?

When the Promise is fulfilled, we expect a string. A Promise can also be rejected; Promise<string> does not promise success and does not describe the type of an error.

Why does <string> appear twice?

Next to the function name, we describe the function's return type. In new Promise<string>, we explicitly describe the Promise being created. TypeScript can infer the second type argument from context here, but we show it so that you can see both places.

What does .then() receive?

The callback receives the fulfilled string value, so TypeScript infers string for message. .then() registers a callback; it does not block the entire program. A variable that stores a Promise does not later turn into a string.

Documentation for this example: MDN — Promise · TypeScript — Everyday Types

EXAMPLE 15 / 19

A Promise with one product

Now the fulfilled value will be an object. This is still a local example, not an HTTP request.

JavaScript

.js file
function loadProduct() {
  return new Promise(function (resolve) {
    resolve({
      id: 17,
      name: "Shelf",
      price: 80
    });
  });
}

loadProduct().then(function (product) {
  console.log(product.name);
  console.log(product.price);
});

TypeScript

.ts file
interface Product {
  id: number;
  name: string;
  price: number;
}

function loadProduct(): Promise<Product> {
  return new Promise<Product>(function (resolve) {
    resolve({
      id: 17,
      name: "Shelf",
      price: 80
    });
  });
}

loadProduct().then(function (product) {
  console.log(product.name);
  console.log(product.price);
});

Output from both versions

Shelf
80

What is the function's result?

loadProduct() produces Promise<Product>. By contrast, the product parameter in .then() has the type Product.

What does TypeScript check in our code?

The object passed to resolve must be compatible with Product. For example, price: "80" would not satisfy the required number type here.

Where is the delay?

There is no timer. The Promise is fulfilled immediately, but the .then() callback still runs asynchronously. A Promise does not imply a mandatory one-second delay.

A distinction to remember

Product is an object. Promise<Product> is a Promise whose fulfilled value is expected to be that kind of object.

Documentation for this example: MDN — Promise

EXAMPLE 16 / 19

A Promise with an array of products

We want to load more than one product. The most important change is Product[] inside Promise.

JavaScript

.js file
function loadProducts() {
  return new Promise(function (resolve) {
    resolve([
      { id: 17, name: "Shelf", price: 80 },
      { id: 18, name: "Coat rack", price: 25 }
    ]);
  });
}

loadProducts().then(function (products) {
  for (const product of products) {
    console.log(product.name);
  }
});

TypeScript

.ts file
interface Product {
  id: number;
  name: string;
  price: number;
}

function loadProducts(): Promise<Product[]> {
  return new Promise<Product[]>(function (resolve) {
    resolve([
      { id: 17, name: "Shelf", price: 80 },
      { id: 18, name: "Coat rack", price: 25 }
    ]);
  });
}

loadProducts().then(function (products) {
  for (const product of products) {
    console.log(product.name);
  }
});

Output from both versions

Shelf
Coat rack

Read it from the inside

Product means one product. Product[] means an array of products. Promise<Product[]> means a Promise with an array as its fulfilled value.

What does the callback receive?

products is already an array. That is why we can iterate over it with for...of. Each product in the loop has the type Product.

Why not use products[0].name?

The loop also works when the array is empty. Reading the first element unconditionally would not be safe for an arbitrary array.

What would a numeric Promise look like?

Use Promise<number> for a price. When there is no useful fulfilled value—for example, after a save operation has finished—you can use Promise<void>.

Documentation for this example: MDN — Promise · TypeScript — Everyday Types

EXAMPLE 17 / 19

async/await: ordinary functions without a manual constructor

An async function automatically returns a Promise. A second function reads the result with await.

JavaScript

.js file
async function loadPrice() {
  return 80;
}

async function showPrice() {
  const price = await loadPrice();
  console.log(price);
}

showPrice().catch(function (error) {
  console.error(error);
});

TypeScript

.ts file
async function loadPrice(): Promise<number> {
  return 80;
}

async function showPrice(): Promise<void> {
  const price = await loadPrice();
  console.log(price);
}

showPrice().catch(function (error: unknown) {
  console.error(error);
});

Output from both versions

80

Why Promise<number> when the function says return 80?

async makes the call return a Promise. The value 80 becomes that Promise's fulfilled value.

What does await do?

It pauses the rest of this asynchronous function until the Promise settles. It does not stop the entire browser. After fulfilment, price is a number.

Why does showPrice have Promise<void>?

It is an async function, but it only displays a message and does not return a useful value. Therefore, its return type is a Promise with void inside it.

Why use catch and unknown?

.catch() handles rejection of the chain. The example is not intended to fail; it simply shows where handling belongs. unknown means that we should check an error before assuming it has a particular structure. Passing it directly to the console is allowed.

Important

Do not wrap fetch in an additional new Promise merely so that you can use await. fetch already returns a Promise.

Documentation for this example: MDN — Promise · TypeScript — Functions

EXAMPLE 18 / 19

fetch: one product and an array of products

This example assumes that your server exposes /api/products/17 for one product and /api/products for an array. These URLs are placeholders—without such a backend, the example cannot load this data.

JavaScript

.js file
async function fetchProduct(id) {
  const response = await fetch("/api/products/" + id);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const data = await response.json();
  return data;
}

async function fetchProducts() {
  const response = await fetch("/api/products");

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const data = await response.json();
  return data;
}

TypeScript

.ts file
interface Product {
  id: number;
  name: string;
  price: number;
}

async function fetchProduct(id: number): Promise<Product> {
  const response = await fetch("/api/products/" + id);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const data = (await response.json()) as Product;
  return data;
}

async function fetchProducts(): Promise<Product[]> {
  const response = await fetch("/api/products");

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const data = (await response.json()) as Product[];
  return data;
}

What does fetch return?

After await fetch(...), we receive a Response object, not the product itself. Among other things, it contains the status and methods for reading the response body.

Why do we check response.ok?

fetch does not reject its Promise merely because the server returns a status such as HTTP 404. If the status does not indicate success, we throw an error ourselves here. A network error or failure to read the JSON can also interrupt the operation.

What does response.json() do?

It asynchronously reads and parses the response body. Valid JSON does not have to be a product—it can describe a completely different structure.

What does as Product mean?

It is an assertion made by the programmer: “treat this data as Product.” It neither converts nor validates the data. Similarly, as Product[] does not validate every element of the array.

What changes for an array?

The function's result is Promise<Product[]>, and the expected data type is Product[]. We do not describe a single product as Product[].

Where should the error be handled?

Call the function inside try/catch when using await, or end the chain with .catch(function (error) { ... }). The example functions report the problem to the calling code.

Important

This is an educational version that uses a type assertion, not complete validation of external data. A safer API boundary accepts unknown and checks the structure, either manually or with a schema validator. fetch does not require as; an assertion is only one possible approach.

Documentation for this example: MDN — Fetch API · TypeScript — Everyday Types

EXAMPLE 19 / 19

Axios: a type supplied to get<T>()

Axios is an additional library. This example assumes that you have run npm install axios and that you have the same custom backend as in the previous example. The code in the lesson is displayed as text—you do not need to load Axios merely to show it on the page.

JavaScript

.js file
import axios from "axios";

async function fetchProduct(id) {
  const response = await axios.get(
    "/api/products/" + id
  );

  return response.data;
}

async function fetchProducts() {
  const response = await axios.get("/api/products");
  return response.data;
}

TypeScript

.ts file
import axios from "axios";

interface Product {
  id: number;
  name: string;
  price: number;
}

async function fetchProduct(id: number): Promise<Product> {
  const response = await axios.get<Product>(
    "/api/products/" + id
  );

  return response.data;
}

async function fetchProducts(): Promise<Product[]> {
  const response = await axios.get<Product[]>(
    "/api/products"
  );

  return response.data;
}

Where is the data?

Axios returns a response object, and the data is stored in response.data. For a typical JSON response, we do not call response.json() here.

What does <Product> do?

axios.get<Product> sets the expected type of response.data. This is another generic, just like StorageBox<number>.

Why is Promise<Product> also present?

axios.get<Product> describes the data inside the Axios response. Promise<Product> describes the result of our own function because we take only response.data from the response and return it.

How do you write the array version?

Use axios.get<Product[]>. axios.get<Product> means one object. Writing Array without an element type does not describe an array of products.

Does Axios validate the interface?

No. Supplying a generic type does not add runtime validation of the response. It is an expected structure for type checking, not proof that the server conforms to it. Handle errors in the calling code.

Important

Types used with fetch and Axios do not replace data validation. Do not claim that as Product or axios.get<Product> guarantees the true structure of the server response.

Documentation for this example: Axios — Response Schema · Axios — TypeScript definitions · TypeScript — Everyday Types

CHEAT SHEET

How to read TypeScript notation

NotationMeaning
numberA number, such as a price.
stringText, such as a product name.
booleantrue or false.
quantity?: numberAn optional number; reading it can produce undefined.
readonly id: numberPrevents assignment through a reference typed this way; it does not freeze the object.
ProductOne object that matches the product description.
Product[]An array of products, which may be empty.
A | BA value that matches at least one of the alternatives.
A & BA value that satisfies the requirements of both types.
function f(x: number): stringA function that accepts a number and returns a string.
voidThe function does not provide a useful result.
<T>A type parameter, not an ordinary function argument.
Promise<Product[]>A Promise whose fulfilled value is expected to be an array of products.
await promiseReads the fulfilled value; rejection must be handled.
as ProductA type assertion with no runtime validation of the data.
axios.get<Product[]>()The expected type of response.data is an array of products; the call must also receive a URL.

QUICK REVIEW

Check what you remember

Answer each question yourself, then open the explanation.

A function accepts two numbers and returns text. Which return type belongs after the parameter list?

string — it is the type of the value returned by the function.

Does console.log("Done") return the text "Done" from a function?

No. console.log displays a value. A function without return finishes with undefined.

An API returns an array of products. How do you type response.data with Axios?

Pass Product[] as the type argument: axios.get<Product[]>(url).

Does getLast<T>(items: T[]): T correctly describe an empty array in every case?

No. An empty array has no last item, so the correct result is T | undefined.

Does as Product validate a server response?

No. It is an assertion for TypeScript, not validation performed at runtime.

What is the type of message inside .then() for Promise<string>?

string — the .then() callback receives the fulfilled value of the Promise.

The key pattern: first identify what a function receives, what it does, and what it returns. Only then choose the types. In many examples, the working code stays the same — only the data description changes.

← Back↑ Back to the top