Hone logo
Hone
Problems

Type-Safe Reactive State Management in TypeScript

This challenge asks you to build a fundamental part of a type-safe reactive programming library. You'll implement a core mechanism for managing observable state and subscribing to its changes, ensuring that type mismatches are caught at compile time. This is crucial for building robust and maintainable applications, especially in large codebases.

Problem Description

Your task is to create a ReactiveState class in TypeScript that allows for the management of state with built-in type safety and reactivity. This class should enable:

  1. Observable State: A value that can be observed for changes.
  2. Type Safety: The state's type should be strictly enforced, preventing accidental assignment of incorrect types.
  3. Subscribers: The ability for multiple listeners (subscribers) to be notified when the state's value changes.
  4. Unsubscribing: Subscribers should be able to detach themselves from notifications.

Key Requirements:

  • ReactiveState<T> Class:
    • Must be generic, accepting a type parameter T for the state's value.
    • Should have a private _value property to store the current state.
    • Should have a private _subscribers property to store a list of functions that will be called upon state changes.
  • Constructor:
    • Accepts an initial value of type T.
    • Initializes _value with the provided initial value.
    • Initializes _subscribers as an empty array.
  • value Getter:
    • A public getter that returns the current _value of type T.
    • Crucially, this getter should NOT allow setting the value directly. The value should only be modifiable via a dedicated set method.
  • set(newValue: T) Method:
    • Accepts a newValue of type T.
    • Updates _value if newValue is different from the current _value.
    • If the value is updated, it must iterate through all registered _subscribers and call each subscriber function.
  • subscribe(callback: (newValue: T, oldValue: T) => void) Method:
    • Accepts a callback function.
    • The callback function will be of type (newValue: T, oldValue: T) => void.
    • Adds the callback function to the _subscribers list.
    • Returns a function (an unsubscribe function) that, when called, removes the specific callback from the _subscribers list.

Expected Behavior:

  • When subscribe is called, the provided callback should be added to the list of subscribers.
  • When set is called with a new value:
    • If the new value is the same as the old value, no subscribers should be notified.
    • If the new value is different, all registered subscribers should be called with the newValue and the oldValue passed as arguments.
  • When the unsubscribe function returned by subscribe is called, the corresponding callback should no longer receive notifications.
  • Attempting to assign a value directly to instance.value should result in a TypeScript compile-time error.

Examples

Example 1: Basic Usage

// Assume ReactiveState class is defined as per requirements

const counter = new ReactiveState<number>(0);

const unsubscribe1 = counter.subscribe((newValue, oldValue) => {
  console.log(`Counter changed from ${oldValue} to ${newValue}`);
});

counter.set(10); // Logs: "Counter changed from 0 to 10"
counter.set(10); // No log, value didn't change

unsubscribe1();
counter.set(20); // No log, subscriber is unsubscribed

Example 2: Multiple Subscribers and Unsubscribing

// Assume ReactiveState class is defined as per requirements

const user = new ReactiveState<{ name: string; age: number }>({ name: "Alice", age: 30 });

const unsubscribeUser1 = user.subscribe((newUser, oldUser) => {
  console.log(`User name changed to: ${newUser.name}`);
});

const unsubscribeUser2 = user.subscribe((newUser, oldUser) => {
  console.log(`User age is now: ${newUser.age}`);
});

user.set({ name: "Bob", age: 31 });
// Logs:
// "User name changed to: Bob"
// "User age is now: 31"

unsubscribeUser1();

user.set({ name: "Charlie", age: 32 });
// Logs:
// "User age is now: 32" (Only the second subscriber gets notified)

Example 3: Type Safety Enforcement

// Assume ReactiveState class is defined as per requirements

const message = new ReactiveState<string>("Hello");

// This will cause a TypeScript compile-time error:
// Type 'number' is not assignable to type 'string'.
// message.set(123);

// This will cause a TypeScript compile-time error:
// Type '{ text: string; }' is not assignable to type 'string'.
// message.set({ text: "World" });

message.set("World"); // This is valid

Constraints

  • The ReactiveState class must be implemented purely in TypeScript.
  • No external reactive libraries (like RxJS, MobX, Zustand) should be used.
  • The set method should only trigger notifications if the new value is strictly different from the old value.
  • The value getter must not allow direct assignment (e.g., state.value = newValue).

Notes

  • Consider how you will manage the list of subscribers. An array is a straightforward approach.
  • When unsubscribing, ensure you correctly identify and remove the specific callback function from the list.
  • The set method's logic for comparing newValue and oldValue should handle primitive types and potentially object references correctly. For complex objects, a shallow comparison is often sufficient for this type of basic reactive pattern, but be mindful of this.
  • Think about the return type of the subscribe method – it needs to be a function that can be called to perform the unsubscription.
Loading editor...
typescript