Compilation
TypeScript code runs on compiler time, in which it’s converted to JavaScript, that runs on execution time.
Create project
Frontend
npm create vite@latest project-name
npm i ts-node-dev -D
//package.json
{
"scripts": {
"dev": "ts-node-dev -- respawn src/index.ts"
}
}
Backend
npm init -y
npm i typescript ts-node-dev
npx tsc --init
//package.json
{
"scripts": {
"dev": "ts-node-dev --respawn src/index.ts",
"build": "npx tsc"
}
}
//tsconfig.json
{
"rootDir": "./src",
"outDir": "./dist",
}
Variables and Types
TypeScript is a strongly typed superset of JavaScript which means it supports data types.
let texto = "Lorem ipsum"; // assigned data type
texto.toLocaleLowerCase(); // ✔️
texto = 2; // ❌
Type Inference
const a: number = 1;
const b: number = 2;
const c = a + b; // TypeScript infers 'c' as a number
Type Assertion
Type assertions allows you to override the type inferred by TypeScript.
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
But there are cases where this could be tricky
const canvas = document.getElementById("span");
if (canvas !== null) {
const ctx = (canvas as HTMLCanvasElement).getContext("2d"); // ❌
}
To fix this we can use instanceof or typeof instead of assuming the data type
const canvas = document.getElementById("span");
if (canvas instanceof HTMLCanvasElement) {
const ctx = canvas.getContext("2d"); // ✔️
}
any Type
Using any disables data type checking, so the variable can have any type.
let obj: any = { x: 0 };
void Type
Using void ensures that a function will never return any value.
function logMessage(message: string): void {
console.log(message);
}
never Type
Using never ensures that a function will never finish its execution and get to the point where it returns something, because it throws an exception or enters an infinite loop before.
function throwError (message: string): never => {
throw new Error(message);
}
ReturnType
ReturnType gives you the type of the return value of a function
function createAddress() {
return {
planet: "Earth",
city: "Barcelona"
};
}
type Address = ReturnType<typeof createAddress>;
const myAddress: Address = {
planet: "Mars",
city: "Mount Olympus"
};
Arrays
Arrays can be used in TypeScript following a set of conventions.
const languages: string[] = [];
languages.push("Javascript"); // ✔️
languages.push(3); // ❌
const languages: Array<string> = [];
languages.push("TypeScript"); // ✔️
languages.push(3); // ❌
const languages: (string | number)[] = [];
languages.push("TypeScript"); // ✔️
languages.push(3); // ✔️
Tuples
A tuple is a typed array with a pre-defined length and types for each index.
type RGB = [number, number, number];
const black: RGB = [0, 0, 0]; // ✔️
const green: RGB = [0, 255, 0]; // ✔️
const pink: RGB = [255, 0, 255, "lol"]; // ❌
Functions
TypeScript allows you to define the types of returns values and function parameters.
function saludar(name: string): string {
// ✔️
console.log(`Hola, ${name}`);
return name;
}
function saludar(persona: { name: string }): string {
// ✔️
const { name } = persona;
console.log(`Hola, ${name}`);
return name;
}
function saludar(name) {
console.log(`Hola, ${name}`); // ❌
}
function saludar(name): number {
console.log(`Hola, ${name}`); // ❌
}
Arrow Functions
const sumar = (a: number, b: number): number => {
// ✔️
return a + b;
};
const restar: (a: number, b: number) => number = (a, b) => {
// ✔️
return a - b;
};
Enums
Enums allow you to define a set of named constants.
const enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
let move: Direction = Direction.Up;
By default, enums assign numeric values starting from 0. But you can specify custom values and this is useful to match them with database values.
const enum Direction {
Up: "up",
Down: "down",
Left: "left",
Right: "right"
}
let move: Direction = Direction.Up;
Type Aliases
Type aliases create a new name for a type. They are useful for simplifying complex type definitions.
type StringOrNumber = string | number;
let value: StringOrNumber;
value = 42; // ✔️
value = "Hello"; // ✔️
Interfaces
Interfaces define the shape of an object. They are similar to type aliases but more suited for defining object structures.
interface Person {
name: string;
age: number;
}
let user: Person = {
name: "John",
age: 30
};
Interfaces can be modified later on and define functions inside them in two different ways.
interface CarritoOps {
add: (product: Producto) => void; // ✔️
remove: (id: number) => void; // ✔️
}
interface CarritoOps {
// ✔️
clear: () => void;
}
Optional Properties
Optional properties are defined with a question mark and are not required for the object to be valid.
interface Car {
make: string;
model: string;
year?: number; // optional property
}
let myCar: Car = {
make: "Toyota",
model: "Corolla"
}; // valid without 'year' ✔️
Read-only Properties
Readonly properties can be set once during initialization but cannot be changed afterward.
interface Book {
title: string;
readonly author: string;
}
let myBook: Book = {
title: "TypeScript Handbook",
author: "Unknown"
};
myBook.title = "Advanced TypeScript"; // ✔️
myBook.author = "John Doe"; // ❌
Template Literal Types
Template literal types produces a new string literal type by concatenating the content.
type id = `${string}-${string}-${string}-${string}-${string}`;
const myId: id = crypto.randomUUID(); // ✔️
const myId: id = "36b8f84d-df4e-4d49-b662-bcde71a8764f"; // ✔️
const myId: id = "123456"; // ❌
Type Indexing
Type Indexing allows you to access and use nested types within a complex type definition.
type HeroProperties = {
isActive: boolean;
address: {
planet: string;
city: string;
};
};
const addressHero: HeroProperties["address"] = {
city: "Madrid",
planet: "Earth"
};
Intersection Types ( & )
Intersection types combine multiple types into one. This allows an object to have multiple type definitions.
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
type ElevatedEmployee = Admin & Employee;
const e1: ElevatedEmployee = {
name: "John",
privileges: ["create-server"],
startDate: new Date()
};
Union Types ( | )
Union types allow a variable to be one of several types.
let identifier: number | string;
identifier = 123; // ✔️
identifier = "ABC"; // ✔️
identifier = true; // ❌
Discriminated Unions
Discriminated unions, also known as tagged unions, are a pattern where each member of a union type has a common, discriminant property.
interface Mario {
company: "Nintendo";
nombre: string;
saltar: () => void;
}
interface Sonic {
company: "Sega";
nombre: string;
correr: () => void;
}
type Personaje = Mario | Sonic;
function jugar(personaje: Personaje) {
if (personaje.company === "Nintendo") {
personaje.saltar(); // ✔️
return;
}
}
Type Guards
Type guards help in narrowing down the type within a conditional block.
interface Mario {
nombre: string;
saltar: () => void;
}
interface Sonic {
nombre: string;
correr: () => void;
}
type Personaje = Mario | Sonic;
function checkIsSonic(personaje: Personaje): personaje is Sonic {
return (personaje as Sonic).correr !== undefined;
}
Generics
Generics provide a way to create reusable components that work with any data type. It’s like the arguments that are passed into a function except the generic allows you to indicate to the component which data type should it expect. We could use any but we would miss the type checking.
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // ✔️ returns a string
let output2 = identity<number>(42); // ✅ returns a number
let output1 = identity<number>("xdxdxd"); // ❌
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
console.log(firstElement<string>(["apple", "banana", "cherry"]));
// ✅ Output: "apple"
console.log(pair<number, string>(42, "answer"));
// ✅ Output: [42, "answer"]
console.log(pair<number, string>(1, 1)); // ❌
interface ProcessIdentity<T, U> {
value: T;
message: U;
process(): T;
}
class processIdentity<X, Y> implements ProcessIdentity<X, Y> {
value: X;
message: Y;
constructor(value: X, message: Y) {
this.value = value;
this.message = message;
}
process(): X {
return this.value;
}
}
const process = new processIdentity("Hola", "Mundo"); // ✔️
const process = new processIdentity("Hola", 123); // ❌
type ValidTypes = string | number;
function identity<T extends ValidTypes, U>(value: T, message: U): T {
let result: T = value + value; // ❌
return result;
}
identity<number, string>(1, "hola");
type ValidTypes = string | number;
function identity<T extends ValidTypes, U>(value: T, message: U) {
let result: ValidTypes = "";
//Type Guards
if (typeof value === "number") {
result = value + value; // suma
} else if (typeof value === "string") {
result = value + value; // concatenación
}
console.log(message);
return result;
}
identity<number, string>(1, "hola"); // ✔️
Conditional Types
Conditional types provide a way to choose one type or another based on a condition.
type MessageOf<T> = T extends { message: string } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>; // string
type DogMessageContents = MessageOf<Dog>; // never
Decorators
Decorators are a special kind of declaration that can be attached to a class, method, accessor, property, or parameter.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Decorators can be used for methods, accessors, properties, and parameters.
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Returned from ${propertyKey} with`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Logs method calls and results
Mapped Types
Mapped types allow you to create new types by transforming existing types.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Todo {
title: string;
description: string;
}
const todo: Readonly<Todo> = {
title: "Learn TypeScript",
description: "Understand advanced concepts"
};
todo.title = "Learn JavaScript"; // ❌
Utility Types
TypeScript provides several utility types to facilitate common type transformations.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type PartialTodo = Partial<Todo>;
type ReadonlyTodo = Readonly<Todo>;
type PickTitle = Pick<Todo, "title">;
let todo: PartialTodo = {
title: "Learn TypeScript"
};
.mts Files
According to TS specifications you can’t use await, import or export inside a .ts file. But you can use them with .mts files.
//getData.ts
const fetchData = async () => {
const response = await fetch("https://api.com/data"); // ❌
const data = await response.json();
return data;
};
//getData.mts
const fetchData = async () => {
const response = await fetch("https://api.com/data"); // ✔️
const data = await response.json();
return data;
};
.d.ts Files
.d.ts files are used to provide type definitions and don’t accept regular code.
interface Hero {
// ❌
name: string;
age: number;
power: string;
}
export interface Hero {
// ✔️
name: string;
age: number;
power: string;
}
Resources
TS Playground - An online editor for exploring TypeScript and JavaScript