Hone logo
Hone
Problems

Typed Actions for Enhanced Angular State Management

This challenge focuses on implementing type-safe actions within an Angular application, typically when using a state management library like NgRx. By defining and using typed actions, you improve code clarity, prevent runtime errors related to incorrect action payloads, and enable better autocompletion and refactoring support.

Problem Description

Your task is to create a set of type-safe actions for a hypothetical Angular application that manages a list of "Tasks". These tasks will have properties like id (number), title (string), and completed (boolean).

You need to define the following actions and ensure they are strongly typed:

  1. Add Task: An action to add a new task to the state.
  2. Toggle Task Completion: An action to change the completed status of an existing task.
  3. Remove Task: An action to remove a task from the state.

Key Requirements:

  • All actions must be defined using a union type of specific action classes or interfaces.
  • Each action class/interface should clearly define its type property (e.g., using a string literal) and any associated payload.
  • The payload for each action must be correctly typed based on the action's purpose.
  • Demonstrate how these typed actions would be dispatched and handled within an Angular component or service (though you don't need to build a full Angular application, just show the relevant TypeScript code).

Expected Behavior:

  • The AddTask action should carry a Task object as its payload.
  • The ToggleTaskCompletion action should carry the id of the task to toggle and optionally the new completed status.
  • The RemoveTask action should carry the id of the task to remove.

Edge Cases:

  • Consider what happens if ToggleTaskCompletion or RemoveTask is called with an id that does not exist. Your action definition should accommodate this, though the reducer logic (which you don't need to implement) would handle the actual behavior.

Examples

Example 1: Defining Actions

// Assume a Task interface is defined elsewhere
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

// Define the AddTask action
class AddTask {
  readonly type = '[Tasks] Add Task'; // Unique string literal type
  constructor(public payload: Task) {}
}

// Define the ToggleTaskCompletion action
class ToggleTaskCompletion {
  readonly type = '[Tasks] Toggle Completion';
  constructor(public payload: { id: number; completed?: boolean }) {}
}

// Define the RemoveTask action
class RemoveTask {
  readonly type = '[Tasks] Remove Task';
  constructor(public payload: { id: number }) {}
}

// Define a union type for all possible Task actions
type TaskActions = AddTask | ToggleTaskCompletion | RemoveTask;

Example 2: Dispatching an Action (Conceptual)

// In an Angular Component or Service
// Assume 'store' is an instance of a state management store (e.g., NgRx Store)

const newTask: Task = { id: 1, title: 'Learn typed actions', completed: false };
const addTaskAction = new AddTask(newTask);
store.dispatch(addTaskAction); // Dispatching the typed action

const taskIdToToggle = 1;
const toggleAction = new ToggleTaskCompletion({ id: taskIdToToggle, completed: true });
store.dispatch(toggleAction);

Example 3: Type Safety in Action Handling (Conceptual)

// In a Reducer or Effect
function handleTaskAction(state: Task[], action: TaskActions): Task[] {
  switch (action.type) {
    case '[Tasks] Add Task':
      // TypeScript knows action.payload is a Task object here
      return [...state, action.payload];

    case '[Tasks] Toggle Completion':
      // TypeScript knows action.payload has id and optional completed
      return state.map(task =>
        task.id === action.payload.id
          ? { ...task, completed: action.payload.completed ?? !task.completed }
          : task
      );

    case '[Tasks] Remove Task':
      // TypeScript knows action.payload has id
      return state.filter(task => task.id !== action.payload.id);

    default:
      return state;
  }
}

Constraints

  • Your solution should be written entirely in TypeScript.
  • You must use string literal types for the type property of your actions.
  • The payload for each action must be correctly typed.
  • Focus on defining the actions and demonstrating their typed usage; full Angular component or reducer implementation is not required, but conceptual code is acceptable.

Notes

  • Consider using classes for actions as they can be instantiated, which is a common pattern in NgRx. Alternatively, you could use factory functions and interfaces.
  • The string literals for action types are important for distinguishing between different actions, especially in reducers.
  • Think about how these typed actions improve maintainability and reduce the likelihood of TypeError exceptions related to incorrect payload structures.
Loading editor...
typescript