Learn TypeScript With Me in 14 Days Day 5

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

Type Aliases and Interfaces in TypeScript

In this part, we’ll dive into Type Aliases and Interfaces in TypeScript. These features will help us organise our code more effectively, especially when dealing with complex data structures. By the end of this post, we will know when to use Type Aliases and Interfaces, and we’ll apply them to our Task Manager project for better type management.

Why Type Aliases and Interfaces?

As your projects grow, you’ll encounter scenarios where you need to model complex types. TypeScript offers two powerful tools for this: Type Aliases and Interfaces. Both help you define types for objects, arrays, and other data structures in a readable and reusable way. However, there are some differences between them, and understanding when to use each is key to writing maintainable code.

Type Aliases vs. Interfaces

  • Type Aliases: Give a name to any type, including primitives, unions, intersections, and objects.
  • Interfaces: Used primarily to describe the shape of objects and allow for extending or merging types. They are more suited to modeling objects and classes.

Type Aliases in TypeScript

A Type Alias allows you to create a new name for any type. This is especially useful for simplifying complex types or creating reusable types across your codebase.

Basic Type Alias Example

Let’s say we often deal with task IDs that can be either a number or a string. Instead of repeating the number | string type everywhere, we can create a type alias:

type TaskId = number | string;

let id1: TaskId = 123;
let id2: TaskId = 'task-456';

Here, TaskId is a type alias for number | string. Now, whenever we need a task ID, we can use TaskId instead of repeating the union type.

Type Alias for a Function Signature

Type aliases are also handy for defining function signatures. Let’s create a type alias for a function that finds a task by its ID:

type FindTask = (id: TaskId) => Task | null;

const findTask: FindTask = (id) => {
  return tasks.find(task => task.id === id) || null;
};

This creates a reusable type FindTask for any function that takes a TaskId and returns either a Task or null.

Interfaces in TypeScript

An Interface defines the structure of an object. It’s like a blueprint that defines what properties an object should have and their types.

Basic Interface Example

We’ve already used an interface for our Task in the Task Manager project. Here’s a quick refresher:

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

let myTask: Task = {
  id: 1,
  title: 'Learn TypeScript',
  completed: false
};

This interface defines that every Task object must have an id (which can be a number or string), a title (string), and a completed status (boolean). TypeScript ensures that any object labeled as a Task conforms to this structure.

Optional Properties in Interfaces

You can also mark properties as optional by adding a ? after the property name. This can be useful if certain properties may not always be present.

interface Task {
  id: TaskId;
  title: string;
  completed: boolean;
  dueDate?: Date; // Optional property
}

let taskWithDueDate: Task = {
  id: 2,
  title: 'Submit assignment',
  completed: false,
  dueDate: new Date('2024-10-10')
};

let taskWithoutDueDate: Task = {
  id: 3,
  title: 'Study for exam',
  completed: true
};

Here, dueDate is an optional property. Some tasks may have a dueDate, while others may not, and TypeScript won’t throw an error if it’s missing.

Extending Interfaces

One of the advantages of interfaces is that they can be extended, meaning we can create new interfaces that build on top of existing ones.

Let’s say we have a special type of task called a PriorityTask that extends a regular Task but adds a priority field:

interface PriorityTask extends Task {
  priority: 'low' | 'medium' | 'high';
}

let urgentTask: PriorityTask = {
  id: 4,
  title: 'Prepare presentation',
  completed: false,
  priority: 'high'
};

Here, PriorityTask extends Task and adds a new field, priority. Now, any object that is a PriorityTask must have all the properties of a Task, plus the priority field.

Combining Type Aliases and Interfaces

We can also combine Type Aliases and Interfaces in our code to structure more complex data. For example, we might want to use a Type Alias for a union type and an Interface for the shape of an object.

Example: Combining Type Aliases and Interfaces

Let’s combine a TaskId (type alias) with a Task (interface) to show how they can work together in our code:

type TaskId = number | string;

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

let task1: Task = {
  id: 1,
  title: 'Learn TypeScript',
  completed: false
};

The type alias TaskId is used inside the Task interface to keep the code clean and reusable.

Applying Type Aliases and Interfaces to the Task Manager

Now, let’s refactor some parts of our Task Manager project by making good use of Type Aliases and Interfaces.

Refactor the removeTask Function

Let’s start by refining the removeTask function to use both the TaskId alias and the Task interface:

function removeTask(id: TaskId): void {
  const taskIndex = tasks.findIndex(task => task.id === id);
  if (taskIndex !== -1) {
    tasks.splice(taskIndex, 1);
    console.log(`Task with id: ${id} has been removed.`);
  } else {
    console.log(`Task with id: ${id} not found.`);
  }
}

removeTask(1);

In this function:

  • We use TaskId to type the id parameter.
  • The function is type-safe, ensuring we only pass a TaskId and modifying the tasks array accordingly.

Refactor the addTask Function

We can also refactor the addTask function using an interface for the Task and type alias for the TaskId:

function addTask(title: string): Task {
  const newTask: Task = {
    id: tasks.length + 1,
    title,
    completed: false
  };
  tasks.push(newTask);
  return newTask;
}

addTask('Study TypeScript');
console.log(tasks);

In this case:

  • The function returns a Task object, which is also added to the tasks array.
  • TypeScript ensures the returned object conforms to the Task interface.

Best Practices for Using Type Aliases and Interfaces

When to Use Type Aliases:

  • Use type aliases when you need to give a name to a primitive, union, or complex function type.
  • Type aliases are perfect for defining unions (e.g., number | string).
  • Use them for function signatures.

When to Use Interfaces:

  • Use interfaces to define the structure of objects.
  • Interfaces are better for extending and building on top of existing types.
  • Use interfaces when working with classes or object-oriented programming.

What’s Next?

Today, we’ve learnt how to use Type Aliases and Interfaces to create more structured and reusable code. We applied these tools to our Task Manager project, making it more type-safe and maintainable.

Tomorrow, we’ll dive into Generics in TypeScript, which will help us create reusable and flexible components. You’ll learn how to apply generics to functions, arrays, and classes.

I hope today’s session has helped you understand what, how and when to use Type Aliases and Interfaces in TypeScript. Tomorrow, we’ll dive into Generics in TypeScript as we prepare to explore object-oriented programming in TypeScript. If you have any questions or comments, feel free contact me at steve [at] stevepopoola.uk.

Useful Resources:

TypeScript Handbook - Type Aliases

TypeScript Handbook - Interfaces