How to exclude a class field from being inherited or somehow override it?

2.5k Views Asked by At

I have a database model (Mongo) and an API model (Apollo). These two are identical except for a 'start' field which is resolved from a string to an object. How to exclude the 'start' field from being inherited or somehow override it?

class Start {
  iso: string;
  local: string;
}

// database model
class BaseThing {
  // a lot of other fields
  start: string; // < - - I don't want to have 'any' type here
}

// API model
class Thing extends BaseThing {
  // a lot of other fields [inherited]
  start: Start; // < - - how to exclude start from being inherited or somehow override it?
}

1

There are 1 best solutions below

1
On BEST ANSWER

This isn't really good practice: subclasses inherit superclass properties and methods. If inheritance isn't desired, it's possible one might really want composition instead of class hierarchies, but that's out of scope for this question. Let's show how to get the behavior you're looking for and then remind everyone at the end that it could lead to weird issues.


If you want to prevent the compiler from seeing the BaseThing constructor as constructing something with a start property, you can use a type assertion to widen the type of BaseThing from (something like) new () => BaseThing to new () => Omit<BaseThing, "start"> using the Omit<T, K> utility type:

class Thing extends (BaseThing as new () => Omit<BaseThing, "start">) {
    start: Start = new Start(); // no error now
}

const thing = new Thing();
console.log(thing.start.iso.toUpperCase()) // ISO

That's a bit ugly looking, but it works. The compiler sees Thing as inheriting everything except start from BaseThing.

If you do this kind of thing often, you could think of refactoring this widening into a helper function like:

class Thing extends OmitCtorProp(BaseThing, "start") {
    start: Start = new Start(); // still okay
}

where

const OmitCtorProp =
    <A extends any[], T extends object, K extends keyof T>(
        ctor: new (...a: A) => T, k: K) =>
        ctor as new (...a: A) => Omit<T, K>;

which can be hidden in a library somewhere.


PLEASE NOTE that, depending on the implementation of the base class, this sort of intentional widening and then incompatible re-narrowing could lead to trouble. Imagine something like this:

class BaseOops {
    val: string = "a string"
    method() {
        console.log(this.val.toUpperCase())
    }
}

const baseOops = new BaseOops();
baseOops.method(); // "A STRING"

So far so good. But then:

class SubOops extends OmitCtorProp(BaseOops, "val") {
    val: number = 12345;
}

const subOops = new SubOops();
subOops.method(); // RUNTIME ERROR!  this.val.toUpperCase is not a function

This fails because the base class's method() depends on val being a string and not anything else. Pseudo-subclasses that change val to some non-string type will be unhappy at runtime unless they also override method(). So anyone who does something like this which intentionally circumvents type safety warnings should take care not to trip over the part of the rug under which the problem has been swept.

Playground link to code