Learn TypeScript With Me in 14 Days Day 9

Posted on 9 October 2024 Reading time: 7 min read
Learn TypeScript With Me in 14 Days Day 9

Day 9: Advanced Types In TypeScript

In this part of the series, we will explore advanced types in TypeScript, such as mapped types, conditional types, intersection types, and type guards. These features allows us to write more flexible, reusable, and powerful code. We’ll break down each concept into digestible examples, so that we can gain good understanding of how they work and how to apply them in practice.

Intersection Types

Intersection types combine multiple types into one, meaning an object must satisfy all combined types.

Basic Example of Intersection Types:

interface Person {
  name: string;
  age: number;
}

interface Employee {
  companyId: string;
  position: string;
}

type EmployeeDetails = Person & Employee;

const employee: EmployeeDetails = {
  name: 'Alice',
  age: 30,
  companyId: '123',
  position: 'Engineer'
};

console.log(employee);

Explanation:

  • Person & Employee: This combines the Person and Employee interfaces, meaning any object of type EmployeeDetails must have all properties from both interfaces.
  • Use Case: Intersection types are useful when creating objects that must conform to multiple contracts (e.g., a person who is also an employee).

Union Types

Union types allow a value to be one of several types, providing flexibility while maintaining type safety.

Union Type Example:

function printInfo(info: string | number): void {
  if (typeof info === 'string') {
    console.log(`Info is a string: ${info}`);
  } else {
    console.log(`Info is a number: ${info}`);
  }
}

printInfo('TypeScript');  // Output: Info is a string: TypeScript
printInfo(101);           // Output: Info is a number: 101

Explanation:

  • string | number: The :info: parameter can either be a :string: or a :number:.
  • Type Narrowing: Using :typeof:, we can differentiate between the types at runtime and handle each accordingly.

Type Guards

Type guards are essential for working with union types. They let you narrow the type within a block of code, ensuring that TypeScript understands which type you’re working with.

Example with instanceof Type Guard:

class Dog {
  bark(): void {
    console.log('Woof!');
  }
}

class Cat {
  meow(): void {
    console.log('Meow!');
  }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

const dog = new Dog();
const cat = new Cat();

makeSound(dog);  // Output: Woof!
makeSound(cat);  // Output: Meow!

Explanation:

  • instanceof: This checks if an object is an instance of a class. TypeScript will automatically narrow down the type based on the result.
  • Use Case: Type guards are useful when handling objects of different types in the same function, ensuring the correct methods are called for each type.

Let’s try and pass something else into makeSound that’s not a Dog or Cat and see what happens.

class Sheep {
  baah(): void {
    console.log('Baah!');
  }
}

const sheep = new Sheep();
makeSound(sheep);  // Argument of type 'Sheep' is not assignable to parameter of type 'Dog | Cat'.

The type guard prevented an object other than a Dog or a Cat from being passed into the function.

Example with “in” Keyword:

interface Car {
  drive(): void;
}

interface Boat {
  sail(): void;
}

function operateVehicle(vehicle: Car | Boat): void {
  if ('drive' in vehicle) {
    vehicle.drive();
  } else {
    vehicle.sail();
  }
}

const car: Car = { drive: () => console.log('Driving a car') };
const boat: Boat = { sail: () => console.log('Sailing a boat') };

operateVehicle(car);   // Output: Driving a car
operateVehicle(boat);  // Output: Sailing a boat

Explanation:

  • ‘property’ in object: This checks whether a property exists in the object, allowing TypeScript to narrow the type.
  • Use Case: The in operator works well when dealing with object types that don’t have an explicit class relationship.

Conditional Types

Conditional types in TypeScript allow you to define types that change based on a condition, enabling you to build more dynamic type logic. It uses syntax similar to ternary operators in JavaScript (condition ? trueValue : falseValue).

Simple Conditional Type Example:

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>;  // true
type Test2 = IsString<number>;  // false

Explanation:

  • T extends string ? true : false: This syntax defines a conditional type. It checks if T is a string, and if so, returns true; otherwise, it returns false.
  • Use Case: Conditional types are powerful when you need to change a type based on specific criteria or conditions.

Example: Conditionally Extract Properties:

interface Task {
  title: string;
  completed: boolean;
}

type ExtractCompleted<T> = T extends { completed: boolean } ? T : never;

type CompletedTask = ExtractCompleted<Task>;  // Task
type NonTask = ExtractCompleted<string>;      // never

Explanation:

  • ExtractCompleted: This type checks if T has a completed property. If it does, it returns T; otherwise, it returns never.
  • Use Case: Conditional types are often used for type filtering, transformation, or inferring specific properties.

Mapped Types

Mapped types allow you to transform the properties of an existing type, applying specific changes to each property. This is extremely useful when creating new versions of a type based on a transformation rule.

Basic Example of Mapped Types:

interface Task {
  title: string;
  completed: boolean;
}

type ReadOnlyTask = {
  readonly [K in keyof Task]: Task[K];
};

const task: ReadOnlyTask = {
  title: 'Complete TypeScript Course',
  completed: false
};

// task.title = 'New Title'; // Error: Cannot assign to 'title' because it is read-only

Explanation:

  • [K in keyof T]: This syntax iterates over each property in Task, and applies a transformation (readonly in this case).
  • Use Case: Mapped types allow you to easily create utility types like Readonly, Partial, or Required.

Example: Partial Type with Mapped Types:

type PartialTask = {
  [K in keyof Task]?: Task[K];
};

const taskUpdate: PartialTask = {
  title: 'Updated Title'  // Only some properties can be updated
};

console.log(taskUpdate);

Explanation:

  • Partial: Here, PartialTask makes all properties of Task optional, so you can pass only the properties you want to update.
  • Use Case: This is useful for creating types where not all properties are required.

Applying Advanced Types to Task Manager

Let’s integrate these advanced types into our Task Manager project to simulate applying TypeScript’s advanced features to real-world scenarios.

Union Types for Task Status:

type TaskStatus = 'completed' | 'pending' | 'archived';

interface Task {
  id: number;
  title: string;
  status: TaskStatus;
}

function getTaskDetails(task: Task): string {
  switch (task.status) {
    case 'completed':
      return `${task.title} is completed.`;
    case 'pending':
      return `${task.title} is pending.`;
    case 'archived':
      return `${task.title} is archived.`;
    default:
      return `${task.title} has an unknown status.`;
  }
}

Mapped Types for Task Transformations:

type ReadOnlyTask = {
  readonly [K in keyof Task]: Task[K];
};

const readonlyTask: ReadOnlyTask = {
  id: 1,
  title: 'Read-only Task',
  status: 'completed'
};

// readonlyTask.title = 'New Title'; // Error: Cannot assign to 'title' because it is a read-only property

Conditional Types for Task Filtering:

type IsHighPriority<T extends Task> = T['status'] extends 'pending' ? true : false;

const task1: IsHighPriority<{ id: 1; title: 'Urgent Task'; status: 'pending' }> = true;
const task2: IsHighPriority<{ id: 2; title: 'Completed Task'; status: 'completed' }> = false;

What’s Next?

In this part, we learnt how to use Advanced Types in TypeScript with in-depth examples. We covered intersection types, union types, type guards, conditional types, and mapped types, and applied them to the Task Manager project. Each example provided practical insights into how you can use these powerful features in your projects.

In the next part, we will deepen our knowledge of TypeScript by exploring TypeScript’s Utility Types If you have any questions or comments, feel free contact me at steve [at] stevepopoola.uk.

Useful Resources

TypeScript Handbook

TypeScript Advanced Types

Mapped Types

Type Guards in TypeScript

Conditional Types