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!