2

I have a base interface that defines an object type in my code and multiple other interfaces that extends it. This base interface will be an attribute for a base class and then the derived class will have the extended interface as attributes. How can I use the extended interfaces in the derived class without always using the as keyword in typescript ?

interface BaseData {
    id: string;
}

interface DerivedDataA extends BaseData {
    derivedFieldA: number;
}

interface DerivedDataB extends BaseData {
    derivedFieldB: boolean;
}

class BaseClass {
   data: BaseData;

   constructor(data: BaseData) {
       this.data = data;
   }
 
   getId() {
      console.log(this.data.id);
   }

   compute() {
    // implement in derived class
   }
}

class DerivedClassA extends BaseClass {
   
   compute() {
       // This is the issue I am having, how to use the derived interface without `as`
       console.log((this.data as DerivedDataA).derivedFieldA);
   } 
}

class DerivedClassB extends BaseClass {
   
   compute() {
       // This is the issue I am having, how to use the derived interface without `as`
       console.log((this.data as DerivedDataB).derivedFieldB);
   } 
}

I have tried using generic types but it doesn't seem to fit as a solution. How would one do this in another language other than typescript? Also is this an anti-pattern/bad practice? Thanks

turtle
  • 69
  • 1
  • 5

1 Answers1

2

If your intent is to narrow the type of a property of a subclass without having any runtime changes, you can do so with the declare property modifier introduced in TypeScript 3.7:

class DerivedClassA extends BaseClass {
    declare data: DerivedDataA; // declared here
    compute() {
        console.log(this.data.derivedFieldA); // okay
    }
}

class DerivedClassB extends BaseClass {
    declare data: DerivedDataB; // declared here
    compute() {
        console.log(this.data.derivedFieldB);
    }
}

Update:

If I recall correctly, Java won't let you make fields covariant (see Wikipedia article on variance for explanation) because they are read-write and the language is stricter than TypeScript. TypeScript takes the (unsound) point of view that property writes don't need to be checked as strictly. This is convenient, but leads to possible uncaught runtime errors:

new DerivedClassA({ id: "oops" })
  .data.derivedFieldA.toFixed(2) // no compiler error, but    
// at runtime  derivedFieldA is undefined!

You can make this a bit less likely in TypeScript with some tweaking:

class DerivedClassA extends BaseClass {
    declare data: DerivedDataA;
   
    // explicit constructor (do the same for B)
    constructor(data: DerivedDataA) {
        super(data);
    }

    compute() {
        console.log(this.data.derivedFieldA);
    }
}

new DerivedClassA({ id: "oops" }); // error!
// -------------> ~~~~~~~~~~~~~~
// Property 'derivedFieldA' is missing 

But it's still not completely safe:

const a = new DerivedClassA({ id: "okay", derivedFieldA: 123 });
const base: BaseClass = a; // acceptable supertype assignability
base.data = { id: "wait a second" }; // acceptable for BaseClass, 
// but this is a DerivedClassA, right? which means:

a.data.derivedFieldA.toFixed(2) // again, no compiler error, but   
// runtime error!  derivedFieldA is undefined 

But that's just the way TypeScript is; these errors turn out to be rare enough in real world code that the TS team decided to allow false negatives and make programming easier. See this answer for more information about the particular interplay between property covariance, aliasing, and property writing. The "right" way to do this would be to use specific variance markers and maybe generic wildcards like in Java, but the TS team thinks that these are too hard to use for most programmers.

Anyway, this sort of subtype property narrowing is not an anti-pattern or bad practice in TypeScript, but if you are used to a sound (or more closely sound) type system like in Java (which has a few holes like this, covariance of built-in arrays is a big one) it might make you feel uneasy. The advice is probably just "use this but try to be careful with it".


Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Great thanks! Could you share what would be the equivalent in other languages like Java or C#, C++ etc? – turtle Jun 14 '21 at 01:15
  • updated a little, but I don't feel like my expertise in Java, C#, C++ is recent enough to be confident about the equivalent, especially because for most of these other languages there is no direct equivalent, as this sort of subtyping is technically unsound. – jcalz Jun 14 '21 at 02:09