Search code examples
angulartypescriptrxjsinterfacebehaviorsubject

Error when converting BehaviourSubjects in Angular


Let's assume we have two interfaces:

interface A {
  name: string;
  _id?: string;
}

interface B {
  name: string;
  _id: string;
}

When assigning them to each other, typescript behaves like I would expect it:

const a: A = { name: 'a' };
const b: B = { name: 'b', _id: '1' };
const c: B = a; // Type A is not assignable to type B
const d: A = b;

const getA = (): A => {
  return b;
};

In line 3, I'm getting the following error:

TS2322: Type A is not assignable to type B
Types of property _id are incompatible.
 Type string | undefined is not assignable to type string
  Type undefined is not assignable to type string

That make sense, because the _id of a could be undefined.

When I use A with an BehaviourSubject likt this, I'm also getting an error:

const behA = new BehaviorSubject<A>(a);
const behB = new BehaviorSubject<B>(b);

const getBehA = (): BehaviorSubject<A> => {
  return behB;
}
S2322: Type BehaviorSubject<B> is not assignable to type BehaviorSubject<A>
Types of property observers are incompatible.
 Type Observer<B>[] is not assignable to type Observer<A>[]
  Type Observer<B> is not assignable to type Observer<A>
   Type A is not assignable to type B
    Types of property _id are incompatible.
     Type string | undefined is not assignable to type string
      Type undefined is not assignable to type string

For me it is not really clear why. If I try the same with a simple container interface, it works fine:

interface Container<T> {
  value: T;
}

const cB: Container<B> = {
  value: {
    name: 'name',
    _id: '1234',
  },
};

const getCA = (): Container<A> => {
  return cB;
}

Solution

  • As the error says:

    ✘ [ERROR] TS2322: Type 'BehaviorSubject<B>' is not assignable to type 'BehaviorSubject<A>'.
      Types of property 'observers' are incompatible.
        Type 'Observer<B>' is not assignable to type 'Observer<A>'.
          Type 'A' is not assignable to type 'B'.
            Types of property '_id' are incompatible.
              Type 'string | undefined' is not assignable to type 'string'.
                Type 'undefined' is not assignable to type 'string'. [plugin angular-compiler]
    
        src/main.ts:79:2:
          79 │   return behB;
             ╵   ~~~~~~
    

    The behavior subject will map perfectly but it extends Subject.

    export declare class BehaviorSubject<T> extends Subject<T> {
      private _value;
      constructor(_value: T);
      get value(): T;
      getValue(): T;
      next(value: T): void;
    }
    

    but it was due to observers which is an array of Observer<T> (extends Subject<T>), so here the type checking is unable to check the individual properties on the properties that were inherited from Subject<T> for some reason (check next point):

    export declare class Subject<T> extends Observable<T> implements SubscriptionLike {
        closed: boolean;
        private currentObservers;
        /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */
        observers: Observer<T>;
        /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */
        isStopped: boolean;
        /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */
        hasError: boolean;
        ...
    

    Inside observers the next property is what gives the error:

    export interface Observer<T> {
      next: (value: T) => void;
      ...
    

    So, the type checking does not work when it is at the callback level, hence you might face this problem. I don't know the exact reason, but this is my analysis.

    Reference Stackblitz