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:
- Observable State: A value that can be observed for changes.
- Type Safety: The state's type should be strictly enforced, preventing accidental assignment of incorrect types.
- Subscribers: The ability for multiple listeners (subscribers) to be notified when the state's value changes.
- Unsubscribing: Subscribers should be able to detach themselves from notifications.
Key Requirements:
ReactiveState<T>Class:- Must be generic, accepting a type parameter
Tfor the state's value. - Should have a private
_valueproperty to store the current state. - Should have a private
_subscribersproperty to store a list of functions that will be called upon state changes.
- Must be generic, accepting a type parameter
- Constructor:
- Accepts an initial value of type
T. - Initializes
_valuewith the provided initial value. - Initializes
_subscribersas an empty array.
- Accepts an initial value of type
valueGetter:- A public getter that returns the current
_valueof typeT. - Crucially, this getter should NOT allow setting the value directly. The value should only be modifiable via a dedicated
setmethod.
- A public getter that returns the current
set(newValue: T)Method:- Accepts a
newValueof typeT. - Updates
_valueifnewValueis different from the current_value. - If the value is updated, it must iterate through all registered
_subscribersand call each subscriber function.
- Accepts a
subscribe(callback: (newValue: T, oldValue: T) => void)Method:- Accepts a
callbackfunction. - The
callbackfunction will be of type(newValue: T, oldValue: T) => void. - Adds the
callbackfunction to the_subscriberslist. - Returns a function (an unsubscribe function) that, when called, removes the specific
callbackfrom the_subscriberslist.
- Accepts a
Expected Behavior:
- When
subscribeis called, the provided callback should be added to the list of subscribers. - When
setis 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
newValueand theoldValuepassed as arguments.
- When the unsubscribe function returned by
subscribeis called, the corresponding callback should no longer receive notifications. - Attempting to assign a value directly to
instance.valueshould 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
ReactiveStateclass must be implemented purely in TypeScript. - No external reactive libraries (like RxJS, MobX, Zustand) should be used.
- The
setmethod should only trigger notifications if the new value is strictly different from the old value. - The
valuegetter 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
setmethod's logic for comparingnewValueandoldValueshould 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
subscribemethod – it needs to be a function that can be called to perform the unsubscription.