Learn TypeScript With Me in 14 Days Day 3

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

Day 3: Arrays and Objects in TypeScript

In this part, we will explore arrays and objects in TypeScript and see how they play a crucial role in organising and structuring our data. By the end of this post, we will have learnt how to define arrays and objects with specific types, and how to apply them to our ongoing Task Manager project.

Why Arrays and Objects?

Arrays and objects are essential data structures in any programming language, especially in JavaScript and TypeScript. They allow us to group multiple pieces of information and store them in a structured format. However, TypeScript adds a layer of type safety by enforcing that the contents of arrays and objects conform to specified types, preventing common runtime errors.

Working with Arrays in TypeScript

Arrays in TypeScript are similar to those in JavaScript, but with the added benefit of type safety. You can specify what type of elements an array will contain using the following syntax:

let numbers: number[] = [1, 2, 3, 4, 5];

Here, numbers is an array of number values. If you try to add a string to this array, TypeScript will throw an error.

The following snippet shows how to define an array of strings for task titles.

let taskTitles: string[] = ['Learn TypeScript', 'Build Task Manager', 'Test the app'];
console.log(taskTitles);

You can push new elements into the array, but TypeScript will ensure that you only push strings.

taskTitles.push('Refactor code'); // Valid
taskTitles.push(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'

Multidimensional Arrays

TypeScript also supports multidimensional arrays, which are arrays of arrays.

let matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

Arrays in the Task Manager Project

Let us now apply arrays to our Task Manager project. We’ll use an array to store multiple tasks, where each task follows the Task interface that we defined earlier.

  1. Create an Array of Tasks: Open the index.ts file and update it with an array of Task objects.
import { Task } from './task';

let tasks: Task[] = [
  { id: 1, title: 'Learn TypeScript', completed: false },
  { id: 2, title: 'Build Task Manager app', completed: false },
  { id: 3, title: 'Test the app', completed: true }
];

console.log(tasks);

Here, tasks is an array that can only hold Task objects. TypeScript ensures that every object added to this array conforms to the Task interface. If you try to add an item that does not conform to the interface, TypeScript will throw an error.

Working with Objects in TypeScript

In TypeScript, objects are structured data that contain key-value pairs, where the keys are strings (or symbols) and the values can be of any type.

Object with Inline Types

You can define an object with inline types, like this:

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

But TypeScript shines when you define an interface for your object structure, as we did in the previously.

Creating Objects Using Interfaces

Let’s revisit the Task interface and create an object using this structure.

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

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

console.log(myTask);

In this example, TypeScript enforces the shape of the object to ensure that it has the id, title, and completed fields with the correct types.

Objects in the Task Manager Project

Refactoring the Task Manager

Now that we have a list of tasks stored in an array, let’s write a few functions to work with this data. We’ll define functions to:

  1. Add a new task.
  2. Toggle the completion status of a task.
  3. Remove a task.

1. Add a New Task

Create a function addTask that adds a new task to the array. This function will receive the title of the task and create a new Task object with a unique id.

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

addTask('Review TypeScript notes');
console.log(tasks);

In this code:

  • The addTask function takes a title as an argument.
  • It creates a new task with a unique id (based on the length of the tasks array).
  • The new task is added to the tasks array.

2. Toggle Task Completion

Now, let’s write a function that toggles the completion status of a task.

function toggleTaskCompletion(taskId: number): void {
  const task = tasks.find(t => t.id === taskId);
  if (task) {
    task.completed = !task.completed;
  }
}

toggleTaskCompletion(2);
console.log(tasks);

Here’s what’s happening:

  • The toggleTaskCompletion function receives a task id.
  • It finds the task in the tasks array using JavaScript’s find() method.
  • If the task is found, it toggles the completed status by flipping the boolean value.

3. Remove a Task

Here is how we remove a task from the task array

function removeTask(id: number): 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(2);
console.log(tasks);

Code explanation:

  • The removeTask function takes an Id of type TaskId (which can be either a number or string).
  • Finding the Task: It uses findIndex() to search for the task with the matching id.
  • Removing the Task: If the task is found (taskIndex !== -1), it is removed from the array using splice(). If not, a message is logged stating that the task was not found.

With these three functions, we have built the foundation for adding and managing tasks in our Task Manager app. Both arrays and objects are now fully utilised and typed in our TypeScript project.

TypeScript’s Readonly and Optional Properties

Readonly Properties

Sometimes, you want to ensure that certain properties of an object can’t be modified after the object is created. TypeScript provides a readonly modifier for this purpose.

interface Task {
  readonly id: number;
  title: string;
  completed: boolean;
}

let newTask: Task = {
  id: 1,
  title: 'Write a blog post',
  completed: false
};

// newTask.id = 2; // Error: Cannot assign to 'id' because it is a read-only property

By adding readonly to the id field, TypeScript ensures that once a task is created, its id cannot be changed.

Optional Properties

Sometimes, not all properties of an object are required. You can define optional properties by adding a ? to the property name in the interface.

interface Task {
  id: number;
  title: string;
  completed?: boolean;
}

let incompleteTask: Task = {
  id: 2,
  title: 'Incomplete task'
};

console.log(incompleteTask); // Output: { id: 2, title: 'Incomplete task' }

In this example, completed is an optional property, meaning the Task object doesn’t need to include it when it’s created.

What’s Next?

Today, we learned how to work with arrays and objects in TypeScript. We used arrays to store multiple tasks and objects to define the structure of each task. With these tools, we can now begin building more complex features in our Task Manager app.

In the next part of the series, we will explore functions in TypeScript in more detail. You’ll learn how to define and type functions, handle optional parameters, and return types, further extending our Task Manager functionality.

Useful Resources:

TypeScript Handbook - Arrays

TypeScript Handbook - Objects

JavaScript find method

JavaScript findIndex method

That’s all for today! See you tomorrow for Day 4: Functions in TypeScript. If you have any questions or run into any issues, you can contact me at steve [at] stephenpopoola.uk