Search code examples
angulartypescriptrxjsobservable

How to map multiple Subject in an object literal to an object literal of corresponding Observable


I am working on a class in Angular and want to share a bunch of states belong to that class. So I make a bunch of BehaviorSubject

private subjects = {
    a : new BehaviorSubject<A>(this.a),
    b : new BehaviorSubject<B>(this.b),
    c : new BehaviorSubject<C>(this.c),
    d : new BehaviorSubject<D>(this.d),
    e : new BehaviorSubject<E>(this.e),
}

To prevent leaking the Observer side of the subjects and only expose the Observable side, I make the subjects private and expose with observables:

observables = {
    a : this.subjects.a.pipe(share()),
    b : this.subjects.b.pipe(share()),
    c : this.subjects.c.pipe(share()),
    d : this.subjects.d.pipe(share()),
    e : this.subjects.e.pipe(share()),
}

I think the observables should be able to be generated from the subjects so that when I want to add more subjects, I don't need to modify the observables manually. Something like:

observables = (()=>{
    let observables : {[Property in keyof typeof this.subjects]:Observable} = {}
    for(let key in this.subjects)
    {
        observables[key] = this.subjects[key as keyof typeof this.subjects].pipe(share())
    }
    return observables;
})();

But here Observable and share do not know their generic type. How can I make this work or if there is better pattern?


Solution

  • As far as I am aware you don't need to share the BehaviorSubject observable as it is already hot via the BehaviorSubject.

    You can solve the generic typing issues by using Mapped types:

    const subjects = {
      a: new BehaviorSubject<number>(1),
      b: new BehaviorSubject<string>('foo'),
      c: new BehaviorSubject<boolean>(false),
    };
    
    // utility type to convert BehaviorSubject<T> to Observable<T> 
    // (can be inlined below instead)
    type BehaviorSubjectToObservable<T> = 
      T extends BehaviorSubject<infer U> ? Observable<U> : never;
    
    // create appropriate type for resulting object which has the same keys
    // but maps to Observable<T>
    // this ensures correct typing within the reduce
    type Observables<T> = {
      [P in keyof T]: BehaviorSubjectToObservable<T[P]>;
    };
    
    // transform the object
    const observables = Object.entries(subjects).reduce(
      (acc, [key, subject]) => ({ ...acc, ...{ [key]: subject.asObservable() } }),
      {} as Observables<typeof subjects>,
    );
    
    // typeof observables.a = Observable<number>
    // typeof observables.b = Observable<string>
    // typeof observables.c = Observable<boolean>