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?
}
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.