Search code examples
angulartypescriptcastingtype-conversiontype-assertion

Typescript casting always returns "object"


Lets say I have two interfaces which have two the same members id, and name:

export interface InterfaceA {
    id: number;
    name: string;
    //some other members
}

export interface InterfaceB {
    id: number;
    name: string;
    //some other members
}

I would like to get collection of elements of both types to populate some combobox. I need id, name and type of every element, so I've made following class

export class AssignableDevice {
    id: number;
    name: string;
    type: string;

    constructor(device: InterfaceA | InterfaceB) {
        this.id = device.id;
        this.name = device.name;
        this.type = typeof device; //still returns "object"
    }
}

// in onInit method : 

ngOnInit() {
    super.ngOnInit();

    this.dataService.getInterfaceA().subscribe((data) => {
      data.forEach((element) => this.devices.push(new AssignableDevice(element as InterfaceA)));
    });

    this.dataService.getInterfaceB().subscribe((data) => {
      data.forEach((element) => this.devices.push(new AssignableDevice(element as InterfaceB)));
    })
}

But problem is I always get "object" in "AssignableDevice" class constructor, and I have no idea why is this happening. I can achieve my goal by using some enum, but I wonder, why this solution is not working, and how could achieve this. I'd rather not make any changes in InterfaceA or InterfaceB.


Solution

  • You can't access the TypeScript type of an object at runtime (in the general case). TypeScript provides a compile-time type system. The typeof that you're using is the JavaScript runtime typeof, which always returns "object" for objects of any kind (and for null).

    You've said you want to send the type to the backend, so you definitely need it at runtime. I can see at least two ways to do that:

    1. You could define your interfaces as branded interfaces to ensure that you always include the type:

      export interface InterfaceA {
          id: number;
          name: string;
          //some other members
          type: "InterfaceA"; // <== This is a _string literal type_ whose only valid value is the string "InterfaceA"
      }
      
      export interface InterfaceB {
          id: number;
          name: string;
          //some other members
          type: "InterfaceB"; // <=== String literal type
      }
      

      Now any object you assign to a variable, property, or parameter of type InterfaceA has to have a type property with the string "InterfaceA" and similar for InterfaceB. Then your code would use that type property.

    2. You could make your constructor private, and only allow creation via createX methods for the interfaces:

      export class AssignableDevice {
          id: number;
          name: string;
          type: string;
      
          private constructor(device: InterfaceA | InterfaceB, type: string) {
              this.id = device.id;
              this.name = device.name;
              this.type = type;
          }
      
          static createA(device: InterfaceA): AssignableDevice {
              return new AssignableDevice(device, "InterfaceA");
          }
      
          static createB(device: InterfaceB): AssignableDevice {
              return new AssignableDevice(device, "InterfaceB");
          }
      }
      

      Now you use the appropriate createX method for the type of the object you have. Since you make that choice when writing the code, TypeScript can typecheck to see that you're passing the right type of object to createX.