JavaScript
.js filelet price = 80;
let productName = "Shelf";
let available = true;
price = 95;
console.log(price);
Nikita Goluban
Full-Stack Developer Logistics & Marketing Mechanical & Construction Skills Woodwork & Metalwork Polyglot
React.js + Next.js · Module 1. TypeScript Basics
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.
PART I
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
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.
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
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
stringText:
let firstName: string = "Anna";
numberWhole 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
booleanA logical value:
let isLoggedIn: boolean = true;
Possible values:
true
false
nullThe intentional absence of a value:
let selectedUser: null = null;
undefinedA value has not been assigned yet:
let result: undefined;
THEORY · 04
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.
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:
THEORY · 05
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
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:
laptopmust be an object that matches theProductinterface.
THEORY · 07
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
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
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
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
A union means:
OR.
Example:
let userId: string | number;
We read this as:
userIdcan be either astringOR anumber.
Therefore:
userId = 15; // ✅
userId = "user15"; // ✅
userId = true; // ❌
We can remember the symbol:
|
as:
OR
THEORY · 12
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
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
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
To begin with, you can follow a simple rule:
interfaceUse it most often to describe objects:
interface User {
id: number;
name: string;
}
typeIt 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
id: number
The function accepts a number as its parameter.
Promise<User>
The function returns a Promise whose fulfilled value is a user.
await fetch(...)
We wait for the server response.
await response.json()
We read the JSON data.
as Useras User
We tell TypeScript:
Treat this value as if it had the
Userstructure.
This is a compile-time assertion, not a runtime check.
THEORY · 36
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
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
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
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
Userobjects at compile time.
It does not check the response data at runtime.
THEORY · 40
async function getProduct(): Promise<Product> {
const response = await fetch("/product");
const data = (await response.json()) as Product;
return data;
}
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
string
number
boolean
null
undefined
string[]
number[]
User[]
interface User {
id: number;
name: string;
}
age?: number;
readonly id: number;
string | number
which means:
string OR number
TypeA & TypeB
which means:
TypeA AND TypeB combined
type Role = "admin" | "user";
function add(a: number, b: number): number {
return a + b;
}
function showMessage(message: string): void {
console.log(message);
}
<T>
= a placeholder for a type.
User
User[]
Promise<User>
Promise<User[]>
axios.get<User>()
axios.get<User[]>()
THEORY · 42
let age: number;
ageis a number.
let name: string;
nameis a string.
const users: User[] = [];
usersis 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>
Boxcan work with different types.
Box<number>
T = number.
Box<string>
T = string.
THEORY · 43
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
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
First, look at three ordinary values. TypeScript adds information about which values should be accepted by the variables.
let price = 80;
let productName = "Shelf";
let available = true;
price = 95;
console.log(price);
let price: number = 80;
let productName: string = "Shelf";
let available: boolean = true;
price = 95;
console.log(price);
95
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.
We added a colon and a type after the variable name: price: number. This does not change how assignment works.
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.
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.
price = "eighty"; // TS error: a string was used instead of a number.
Documentation for this example: TypeScript — Everyday Types
EXAMPLE 02 / 19
We want to add the delivery cost to the cost of the items. We begin with an untyped function and then add the types.
function addDelivery(itemsCost, deliveryCost) {
return itemsCost + deliveryCost;
}
const total = addDelivery(80, 15);
console.log(total);
function addDelivery(
itemsCost: number,
deliveryCost: number
): number {
return itemsCost + deliveryCost;
}
const total = addDelivery(80, 15);
console.log(total);
95
itemsCost receives 80, and deliveryCost receives 15. These are two parameters; we pass arguments to them when we call the function.
return itemsCost + deliveryCost; sends the result of the addition back to the place where the function was called. The total variable receives 95.
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.”
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.
addDelivery("80", 15); // TS error: the first argument must be a number.
Documentation for this example: TypeScript — Everyday Types
EXAMPLE 03 / 19
Not every function performs a calculation. One function can prepare a label, while another answers whether a product is available.
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));
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));
Product: Coat rack
true
false
makeLabel accepts a string and returns a string. Both types are string, but the first describes the parameter and the second describes the result.
quantity > 0 is a comparison. Its result is true or false, so we write boolean after the parameter list, not number.
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
This function only displays a message. It does not prepare a result for use in further calculations.
function showNotice(message) {
console.log(message);
}
const result = showNotice("Product saved");
console.log(result);
function showNotice(message: string): void {
console.log(message);
}
const result = showNotice("Product saved");
console.log(result);
Product saved
undefined
The first message comes from inside the function. The second line reads the value received by the result variable.
console.log displays something, but it does not replace return. In JavaScript, an ordinary function that finishes without returning a value produces undefined.
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
A package description must always contain a name. We add the weight only when it is provided.
function describePackage(name, weight) {
if (weight === undefined) {
return name;
}
return name + ": " + weight + " kg";
}
console.log(describePackage("Package A", 7));
console.log(describePackage("Package B"));
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"));
Package A: 7 kg
Package B
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.
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.
That condition would also treat 0 as false. Comparing with undefined specifically checks for a missing value, not whether the value is “truthy.”
In an interface, weight?: number also describes an optional property. Reading a property that is not present can produce undefined.
describePackage("Package C", "7"); // TS error: string was used instead of number.
Documentation for this example: TypeScript — Functions
EXAMPLE 06 / 19
The function will create an object that describes a product. An interface lets us give the expected structure a name.
function createProduct() {
return {
id: 17,
name: "Shelf",
price: 80
};
}
const product = createProduct();
console.log(product.name);
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);
Shelf
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.
Only the return { ... }; statement creates and returns the data. : Product describes its expected structure while the code is being checked.
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.
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.
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
We have a list of products. We want to receive only their names, still as an array.
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(", "));
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(", "));
Shelf, Coat rack
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[].
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.
It is an ordinary anonymous function passed as a callback. map calls it for each array element. We are not using an arrow function.
From Product[], TypeScript infers that product has the type Product. There is no need to repeat that information.
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
Delivery has two options. The function accepts the name of an option and creates a description.
function describeDelivery(method) {
if (method === "pickup") {
return "Store pickup";
}
return "Courier delivery";
}
console.log(describeDelivery("pickup"));
console.log(describeDelivery("courier"));
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"));
Store pickup
Courier delivery
We give a type description a name. Here, DeliveryMethod means exactly "pickup" | "courier".
| means “or.” We allow the string "pickup" or the string "courier", not just any string. These specific strings are literal types.
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".
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.
describeDelivery("bicycle"); // TS error: this option is not permitted.
Documentation for this example: TypeScript — Everyday Types
EXAMPLE 09 / 19
One type describes a product name, while another describes the number of units. The function needs both pieces of information.
function describeStock(item) {
return item.name + ": " + item.quantity + " units";
}
const shelf = {
name: "Shelf",
quantity: 6
};
console.log(describeStock(shelf));
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));
Shelf: 6 units
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.
No. It is an operation on types. We create the data separately in const shelf = { ... }. The type will not add a missing property.
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.
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.
describeStock({ name: "Shelf" }); // TS error: quantity is missing.
Documentation for this example: TypeScript — Object Types
EXAMPLE 10 / 19
The code should use named delivery states. In JavaScript, we show a similar set in an object; in TypeScript, we use an enum.
const DeliveryState = {
Prepared: "PREPARED",
Sent: "SENT",
Received: "RECEIVED"
};
function showDeliveryState(state) {
return "Status: " + state;
}
console.log(showDeliveryState(DeliveryState.Sent));
enum DeliveryState {
Prepared = "PREPARED",
Sent = "SENT",
Received = "RECEIVED"
}
function showDeliveryState(state: DeliveryState): string {
return "Status: " + state;
}
console.log(showDeliveryState(DeliveryState.Sent));
Status: SENT
DeliveryState.Sent produces the string "SENT". The name Sent and its assigned value are two different things.
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.
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.
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.
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
We want to name this requirement: a function accepts two numbers and returns a number. We still implement it with function.
function multiplyPrice(price, quantity) {
return price * quantity;
}
const operation = multiplyPrice;
console.log(operation(25, 4));
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));
100
It describes how a function is called: two numeric arguments and a numeric result. It does not perform any multiplication.
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.
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
This function returns the last element of an array. It should work with both numbers and strings.
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([]));
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>([]));
46
Birch
undefined
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.
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.
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.
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.
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
We will create a function that packs a value into an object. The content property can hold different kinds of data.
function packValue(value) {
return {
content: value
};
}
const numberBox = packValue(46);
const textBox = packValue("Ash");
console.log(numberBox.content);
console.log(textBox.content);
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);
46
Ash
StorageBox<T> describes an object with a content property of type T. We do not need to create separate NumberBox and TextBox interfaces.
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.
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.
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
We will simulate waiting for a message. This example works without a server. All callbacks are ordinary functions.
function loadNotice() {
return new Promise(function (resolve) {
setTimeout(function () {
resolve("Warehouse ready");
}, 500);
});
}
loadNotice().then(function (message) {
console.log(message);
});
function loadNotice(): Promise<string> {
return new Promise<string>(function (resolve) {
setTimeout(function () {
resolve("Warehouse ready");
}, 500);
});
}
loadNotice().then(function (message) {
console.log(message);
});
Warehouse ready
It immediately returns a Promise object, not a string. The function passed to the new Promise constructor runs immediately and schedules a timer.
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.
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.
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.
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.
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
Now the fulfilled value will be an object. This is still a local example, not an HTTP request.
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);
});
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);
});
Shelf
80
loadProduct() produces Promise<Product>. By contrast, the product parameter in .then() has the type Product.
The object passed to resolve must be compatible with Product. For example, price: "80" would not satisfy the required number type here.
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.
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
We want to load more than one product. The most important change is Product[] inside Promise.
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);
}
});
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);
}
});
Shelf
Coat rack
Product means one product. Product[] means an array of products. Promise<Product[]> means a Promise with an array as its fulfilled value.
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.
The loop also works when the array is empty. Reading the first element unconditionally would not be safe for an arbitrary array.
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
An async function automatically returns a Promise. A second function reads the result with await.
async function loadPrice() {
return 80;
}
async function showPrice() {
const price = await loadPrice();
console.log(price);
}
showPrice().catch(function (error) {
console.error(error);
});
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);
});
80
async makes the call return a Promise. The value 80 becomes that Promise's fulfilled value.
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.
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.
.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.
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
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.
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;
}
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;
}
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.
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.
It asynchronously reads and parses the response body. Valid JSON does not have to be a product—it can describe a completely different structure.
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.
The function's result is Promise<Product[]>, and the expected data type is Product[]. We do not describe a single product as Product[].
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.
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 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.
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;
}
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;
}
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.
axios.get<Product> sets the expected type of response.data. This is another generic, just like StorageBox<number>.
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.
Use axios.get<Product[]>. axios.get<Product> means one object. Writing Array without an element type does not describe an array of products.
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.
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
| Notation | Meaning |
|---|---|
number | A number, such as a price. |
string | Text, such as a product name. |
boolean | true or false. |
quantity?: number | An optional number; reading it can produce undefined. |
readonly id: number | Prevents assignment through a reference typed this way; it does not freeze the object. |
Product | One object that matches the product description. |
Product[] | An array of products, which may be empty. |
A | B | A value that matches at least one of the alternatives. |
A & B | A value that satisfies the requirements of both types. |
function f(x: number): string | A function that accepts a number and returns a string. |
void | The 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 promise | Reads the fulfilled value; rejection must be handled. |
as Product | A 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
Answer each question yourself, then open the explanation.
string — it is the type of the value returned by the function.
No. console.log displays a value. A function without return finishes with undefined.
Pass Product[] as the type argument: axios.get<Product[]>(url).
No. An empty array has no last item, so the correct result is T | undefined.
No. It is an assertion for TypeScript, not validation performed at runtime.
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.