TypeScript: A Gentle Introduction with To-Do List Example

Pedro Laracuente
typescriptjavascripttutorial

TypeScript builds on JavaScript by adding static types. This catches errors during development instead of at runtime. We'll explore core TypeScript concepts by building a to-do list application.


What Is TypeScript?

TypeScript is a superset of JavaScript. Any valid JavaScript code is valid TypeScript code. TypeScript adds optional static typing, letting you specify types for variables, function parameters, and return values. This catches mistakes in your editor or during compilation.


Why Use TypeScript?

TypeScript brings practical benefits:

  • Early Error Detection: Catch type errors at compile time instead of production.
  • Improved Readability: Explicit types make code self-documenting.
  • Better Maintainability: Type system prevents bugs during refactoring.
  • Enhanced Tooling: IDEs provide better auto-completion, navigation, and refactoring.

Core Concepts Through a To-Do List Example

Let's break down some fundamental TypeScript concepts and see how they apply to a simple to-do list application.

1. Basic Types

TypeScript's built-in types:

  • `string` for text: `"Buy groceries"`
  • `number` for numeric values: `1`, `2.5`, `-10`
  • `boolean` for true/false
  • `null` and `undefined` for absence of value
  • `any` to bypass type checking (use sparingly)
  • `void` for functions with no return value

Consider these examples:

let taskName: string = "Buy groceries";
let taskId: number = 1;
let isCompleted: boolean = false;
let notes: string | null = null; // Union type

In our to-do list application, you might also have:

let taskDescription: string = "Pick up milk, eggs, and bread.";
let priorityLevel: number = 2; // 1=high, 2=medium, 3=low
let isTaskComplete: boolean = false;

Explicit types reduce unexpected behavior.


2. Arrays

Arrays store values of a single type:

let tasks: string[] = ["Buy groceries", "Walk the dog", "Pay bills"];
let completedTasks: boolean[] = [false, true, false];

For our to-do list, you could have a simple array of task descriptions:

let todoItems: string[] = ["Clean the kitchen", "Finish report", "Call mom"];

This prevents mixing data types.


3. Objects

Objects group related data. Define their shape using `interface` or type alias.

Using an interface:

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

let myTask: Task = {
  id: 1,
  name: "Grocery Shopping",
  completed: false,
};

Alternatively, a type alias for a user might look like:

type User = {
  id: number;
  name: string;
};

For our to-do list app, a `TodoItem` might be defined as:

interface TodoItem {
  id: number;
  text: string;
  isCompleted: boolean;
  dueDate?: Date; // Optional
}

let firstTask: TodoItem = {
  id: 1,
  text: "Learn TypeScript",
  isCompleted: false,
};

This structure organizes code and ensures type consistency.


4. Functions

TypeScript lets you define function parameter types and return types:

function addTask(taskName: string, priority: number): void {
  console.log(\`Adding task: \${taskName} with priority \${priority}\`);
}

To-do list functions:

function addTodo(todoText: string, todos: TodoItem[]): TodoItem[] {
    const newTodo: TodoItem = {
        id: todos.length + 1,
        text: todoText,
        isCompleted: false,
    };
    return [...todos, newTodo];
}

function markAsComplete(todoId: number, todos: TodoItem[]): TodoItem[] {
    return todos.map(todo =>
        todo.id === todoId ? { ...todo, isCompleted: true } : todo
    );
}

Return types clarify function output.


5. Enums

Enums define named constants for fixed value sets:

enum TaskPriority {
  High,   // Defaults to 0
  Medium, // Defaults to 1
  Low,    // Defaults to 2
}

let myPriority: TaskPriority = TaskPriority.Medium;

Integrating enums into interfaces:

enum Priority {
  High,
  Medium,
  Low,
}

interface TodoItem {
  id: number;
  text: string;
  isCompleted: boolean;
  priority: Priority;
  dueDate?: Date;
}

let urgentTask: TodoItem = {
  id: 2,
  text: "Submit project proposal",
  isCompleted: false,
  priority: Priority.High,
};

Enums replace arbitrary numbers with meaningful names.


6. Union and Intersection Types

TypeScript combines types flexibly.

Union Types allow multiple type options:

let result: number | string;
result = 10;     // Valid
result = "Hello"; // Valid
// result = true; // Error

Intersection Types merge multiple types:

interface Person {
  name: string;
}

interface Contact {
  email: string;
  phone: string;
}

type Employee = Person & Contact;

let newEmployee: Employee = {
  name: "John Doe",
  email: "john.doe@example.com",
  phone: "555-1234",
};

These techniques model complex data while maintaining type safety.


7. Type Assertion

Type assertions tell the compiler a variable's type when you know better. Use with caution:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

Use type assertions only when certain, not to bypass type safety.


8. Generics

Generics write functions that work across data types while maintaining type checking.

function identity<T>(arg: T): T {
    return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);

Generic function for retrieving first item:

function getFirstItem<T>(items: T[]): T | undefined {
    return items.length > 0 ? items[0] : undefined;
}

// Usage:
let firstTodo = getFirstItem<TodoItem>(arrayOfTodoItems);

Generics enable flexible, reusable code with type guarantees.


Conclusion

We've covered essential TypeScript concepts: basic types, arrays, objects, functions, enums, union/intersection types, type assertions, and generics. Integrate these features into your projects to catch errors earlier and write maintainable code.

Experiment with these concepts and build projects. For advanced topics, see the official TypeScript documentation.