TypeScript
TypeScript has its own universe, its own particularities, and basically, it's a language based on JavaScript. It adds a whole bunch of types, a whole bunch of features, to be able to determine the type of data, and make sure we don't have errors in our code.
Resource: TypeScript Roadmap
Why TypeScript?
- Error detection at compilation - Rather than at runtime
- Better IntelliSense - Autocompletion and suggestions in the IDE
- More maintainable code - Especially on large projects
- Living documentation - Types serve as documentation
Basic Types
Primitive Types
// Basic types
let name: string = 'bernard';
let age: number = 25;
let isActive: boolean = true;
let value: null = null;
let notDefined: undefined = undefined;
// BigInt and Symbol (less common)
let bigNumber: bigint = 123n;
let uniqueId: symbol = Symbol('id');
Arrays
// Different syntaxes for arrays
let numbers: number[] = [1, 2, 3, 4];
let names: Array<string> = ['Alice', 'Bob', 'Charlie'];
let mixed: (string | number)[] = ['hello', 42, 'world'];
// Read-only array
let readonlyNumbers: readonly number[] = [1, 2, 3];
Objects
// Simple object
let user: { name: string; age: number } = {
name: 'John',
age: 30
};
// Optional properties
let config: { host: string; port?: number } = {
host: 'localhost'
// port is optional
};
// Index signature (dynamic keys)
let scores: { [key: string]: number } = {
math: 95,
english: 87
};
Advanced Types
Interfaces
// Definition of an interface
interface User {
id: number;
name: string;
email: string;
isActive?: boolean; // Optional property
readonly createdAt: Date; // Read-only property
}
// Usage
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date()
};
// Interface extension
interface AdminUser extends User {
permissions: string[];
lastLogin?: Date;
}
Union and Intersection Types
// Union (OR) - can be one or the other
type Status = 'pending' | 'approved' | 'rejected';
type ID = string | number;
let currentStatus: Status = 'pending';
let userId: ID = 123; // or "user_123"
// Intersection (AND) - must have both
type Person = { name: string; age: number };
type Employee = { company: string; salary: number };
type WorkingPerson = Person & Employee;
const worker: WorkingPerson = {
name: 'Bob',
age: 30,
company: 'TechCorp',
salary: 50000
};
Enums
// Numeric enum
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// Enum with custom values
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500
}
// String enum
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue'
}
// Usage
let direction: Direction = Direction.Up;
let response: StatusCode = StatusCode.Success;
Tuples
// Tuple - array with fixed types and size
let coordinate: [number, number] = [10, 20];
let userInfo: [string, number, boolean] = ['Alice', 25, true];
// Tuple with labels (more readable)
let point: [x: number, y: number] = [10, 20];
// Optional tuple
let rgb: [number, number, number?] = [255, 0]; // Blue is optional
Functions
Function Typing
// Simple function
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Optional parameter
function greet(name: string, title?: string): string {
return title ? `Hello ${title} ${name}` : `Hello ${name}`;
}
// Default parameter
function createUser(name: string, role: string = 'user'): User {
return { id: Date.now(), name, email: '', createdAt: new Date() };
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
Function Types
// Function type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
// Interface for function
interface Calculator {
(operation: string, a: number, b: number): number;
}
Generics
// Simple generic
function identity<T>(arg: T): T {
return arg;
}
let stringResult = identity<string>('hello'); // Type: string
let numberResult = identity<number>(42); // Type: number
// Generic with constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength('hello'); // ✅ string has a length property
logLength([1, 2, 3]); // ✅ array has a length property
// logLength(123); // ❌ number does not have a length property
// Generic with interfaces
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial - all properties become optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; password?: string; }
// Required - all properties become mandatory
type RequiredUser = Required<PartialUser>;
// Pick - select certain properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string; }
// Omit - exclude certain properties
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; }
// Record - create a type with specific keys and values
type Roles = Record<string, string[]>;
// { [key: string]: string[]; }
Classes
class Animal {
protected name: string; // Accessible in the class and its children
private age: number; // Accessible only in this class
public species: string; // Accessible everywhere (by default)
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this.species = species;
}
// Public method
public makeSound(): void {
console.log(`${this.name} makes a sound`);
}
// Getter
get info(): string {
return `${this.name} is ${this.age} years old`;
}
// Setter
set updateAge(newAge: number) {
if (newAge > 0) {
this.age = newAge;
}
}
}
// Inheritance
class Dog extends Animal {
private breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age, 'Dog');
this.breed = breed;
}
public makeSound(): void {
console.log(`${this.name} barks!`);
}
}
// Abstract class
abstract class Shape {
abstract area(): number;
display(): void {
console.log(`Area: ${this.area()}`);
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
Literal and Conditional Types
// Literal types
type Theme = 'light' | 'dark';
type Size = 'small' | 'medium' | 'large';
// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type Example1 = NonNullable<string | null>; // string
type Example2 = NonNullable<number | undefined>; // number
// Mapped types
type Optional<T> = {
[P in keyof T]?: T[P];
};
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
Practical Examples
API with TypeScript
// Types for a REST API
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
interface ApiError {
code: string;
message: string;
details?: any;
}
// API Service
class UserService {
async createUser(userData: CreateUserRequest): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message);
}
return response.json();
}
async getUsers(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}
}
Application State with TypeScript
// Types for state management
interface AppState {
user: User | null;
loading: boolean;
error: string | null;
theme: Theme;
}
type Action =
| { type: 'SET_USER'; payload: User }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string }
| { type: 'SET_THEME'; payload: Theme };
function appReducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
default:
return state;
}
}
TypeScript Configuration
Basic tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"jsx": "preserve"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Practical Tips
✅ Good Practices
// ✅ Use explicit names
interface UserProfile {
firstName: string;
lastName: string;
email: string;
}
// ✅ Prefer interfaces to types for objects
interface Config {
apiUrl: string;
timeout: number;
}
// ✅ Use union types for limited values
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// ✅ Document with JSDoc comments
/**
* Calculates the distance between two points
* @param point1 First point
* @param point2 Second point
* @returns Distance in pixels
*/
function calculateDistance(
point1: [number, number],
point2: [number, number]
): number {
// Implementation...
return 0;
}
❌ To Avoid
// ❌ Avoid 'any' as much as possible
let data: any = fetchData(); // Loses all the benefits of TypeScript
// ❌ Don't overuse type assertions
let user = data as User; // Dangerous if data is not really a User
// ❌ Avoid overly complex types
type ComplexType<T, U, V> = T extends U ? V extends string ? boolean : never : T;
TypeScript may seem intimidating at first, but it quickly becomes essential for maintaining robust and scalable JavaScript code!