How do you call a subclass method in the baseclass method? - javascript

I am sorry if this question is so confusing, I am going to try and be as simple as possible.
TLDR; How, in a Typescript baseclass, do you call the private method of a subclass that has to be defined in the subclass?
I have many different Typescript subclasses that require different code to do the same thing: write to a file.
90% of the base code is the same, with the specific implementation of some methods per subclass.
So I have taken the same code and put it in a base class, but that calls a method that has to be implemented on the subclass and it is preferably private.
Here is an example:
abstract class BaseClass {
constructor() { }
SetOptions() {
// Set the options
// Return the options
}
GetOptions() {
// Get the options
this.WriteStuff();
}
private WriteStuff(arg1: Item, arg2: number) {}
}
class SubClass1 extends BaseClass {
private WriteStuff(arg1: SubClass1Item, number: number) {
// Write stuff in the SubClass1 way
}
}
class SubClass2 extends BaseClass {
private WriteStuff(arg1: SubClass2Item, number: number) {
// Write stuff the SubClass2 way
}
}
SubClass1Item and SubClass2Item implement the Item interface.
All subclasses will use the same code for SetOptions and GetOptions, but WriteStuff will have individualized code in each subclass and needs to be private so it isn't called by other devs on accident.
Typescript says that I can't do this because "Types have separate declarations of a private property 'WriteStuff'". And Typescript won't let me not declare the method in the BaseClass as then I am calling a method that doesn't exist but will exist in the subclass.
Is this possible?

Private vs. Protected
private methods can only be accessed in the class itself, not in any descendants. You cannot have a private abstract method because this is an inherent contradiction -- if descendants need to implement it then it is not private.
You want to use the protected modifier. Per the docs:
The protected modifier acts much like the private modifier with the exception that members declared protected can also be accessed within deriving classes.
You want to declare in your BaseClass that all concretions must implement a WriteStuff() method. This is an abstract method meaning that it has no implementation in BaseClass and must be overwritten.
In your descendants, the implementation of WriteStuff must be protected rather than private.
Code
abstract class BaseClass {
/* .... */
protected abstract WriteStuff(arg1: Item, arg2: number): void;
}
class SubClass1 extends BaseClass {
protected WriteStuff(arg1: SubClass1Item, number: number) {
// Write stuff in the SubClass1 way
}
}
Playground Link
arg1 Type
Your classes don't all share an identical signature because they each have a different type for the first argument arg1 passed to WriteStuff. You most likely want to use a generic class based on the type of arg1 or some other value.
If SubClass1Item and SubClass2Item do not extend Item then you absolutely need a generic class because the implementations of WriteStuff in the subclasses would not be assignable to the base.
If they do extend Item then you will not get typescript errors with the current setup. However there is potential for runtime errors when you are calling this.WriteStuff from GetOptions() in BaseClass. How does the BaseClass know if the Item that it has is assignable to the Item subtype that is expected in SubClass1 or SubClass2? This is where generics can help.
We make the BaseClass a generic based on a variable ItemType. We can require that all ItemType variables extend a shared base Item, but we don't have to.
The subclasses are not themselves generic classes. They will extend a version of BaseClass with the specific ItemType that they require.
Code
abstract class BaseClass<ItemType extends Item> {
/* ... */
GetOptions(item: ItemType) {
// Get the options
this.WriteStuff(item, 5);
}
protected abstract WriteStuff(arg1: ItemType, arg2: number): void;
}
class SubClass1 extends BaseClass<SubClass1Item> {
protected WriteStuff(arg1: SubClass1Item, number: number) {
// Write stuff in the SubClass1 way
}
}
Playground Link

JavaScript lately supports private field declarations including private instance methods. There, a possible approach would provide the sub class specific implementation to the base class' super call ...
class BaseClass {
// default implementation of private instance method.
#writeStuff = () => void 0;
constructor(writeStuff) {
// at instantiation time, if appropriate, ...
if (typeof writeStuff === 'function') {
// ... overwrite the default implementation.
this.#writeStuff = writeStuff;
}
}
// prototypal method ...
getOptions(...args) {
// ... with access to a private instance method.
this.#writeStuff(...args);
}
setOptions() {/* ... */}
}
class SubClass1 extends BaseClass {
constructor() {
const writeStuff = (...args) =>
// Write stuff in the SubClass1 way
console.log(`SubClass1 [...args] : ${ [...args] }`);
super(writeStuff);
// do other things
}
}
class SubClass2 extends BaseClass {
constructor() {
const writeStuff = (...args) =>
// Write stuff in the SubClass2 way
console.log(`SubClass2 [...args] : ${ [...args] }`);
super(writeStuff);
// do other things
}
}
const objA = new SubClass1;
const objB = new SubClass2;
objA.getOptions('foo', 'bar');
objB.getOptions('baz', 'biz');
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related

Typescript - can't access properties in child class via Object.keys()

I am trying to use the same logic declared in constructor of base class for all child classes.
In more details, I want to iterate over all class attributes from it's constructor.
My expectation was that if I invoke Object.keys() in base constructor and call super() in child's constructor - it will work for me.
Code example:
class BaseClass {
private baseField1: string;
private baseField2: string;
constructor(data: any) {
console.log(this.constructor.name + " has fields: " + Object.keys(this));
}
}
class ChildClass extends BaseClass {
private childField1: string;
constructor(data: any) {
super(data);
}
}
let base = new ChildClass({name: 'Joe'});
I expect to have output:
ChildClass has fields: baseField1,baseField2,childField1
But in fact I have:
ChildClass has fields: baseField1,baseField2
There is no childField1 in output
Any idea on how this is better to sort out?
UPD:
Finally I made it work as I expect. Thanks to Touffy!
class BaseClass {
private baseField1: string;
private baseField2: string;
constructor(data: any) {
Object.assign(this, data);
}
}
class ChildClass extends BaseClass {
private childField1: string;
constructor(data: any) {
super(data);
if (data.childField1) this.childField1 = data.childField1;
}
}
let base = new ChildClass({
baseField1: 'bf1',
baseFileds2: 'bf2',
childField1: 'Joe'
});
console.log('Class fields: ' + JSON.stringify(base));
Output is: Class fields: {"baseField1":"bf1","baseFileds2":"bf2","childField1":"Joe"}
The issue is not with Object.keys, but the timing of when it is called in the constructor chain. You wrote the call of Object.keys in the parent constructor, which is called by super in the child constructor before any child properties are initialized.
You need to remember that those nice property declarations are merely syntactic sugar, equivalent to this.propertyName = undefined in the constructor. Those lines are evaluated after the call to super (the parent constructor). In other words, what the JavaScript interpreter will really do is this :
class BaseClass {
constructor(data) {
this.baseField1 = undefined;
this.baseField2 = undefined
console.log(this.constructor.name + " has fields: " + Object.keys(this));
}
}
class ChildClass extends BaseClass {
constructor(data) {
BaseClass.call(this, data);
this.childField1 = undefined // too late !
}
}
If you call Object.keys in the child constructor after super(), or anywhere that's not in a constructor, it will work as expected.
However, if your goal is specifically to log all the properties of a child instance immediately when it is constructed, and have this behaviour inherited from the parent class… I don't think you can, even with Proxies.
constructor(data: any) -- this is not OOP, this is bad JavaScript habits under disguise.
Be as specific as possible with the interface of your class. Let it ask exactly what values it needs in the constructor, not just some opaque object.
class BaseClass {
public constructor(
private baseField1: string,
private baseField2: string,
) {
// Object.keys(this) is not a good idea if the class has methods
console.log(this.constructor.name + " has fields: " + Object.keys(this));
}
}
class ChildClass extends BaseClass {
public constructor(
baseField1: string,
baseField2: string,
private childField1: string;
){
super(baseField1, baseField2);
}
}
// The compiler does not allow you to pass only one argument to the constructor
// It ensures the object integrity
let base = new ChildClass('Joe');

Subclassing in TypeScript and using more specific properties or methods

In TypeScript, how can I get a more specific subclass of a more generic class to allow referencing more specific properties and methods of that subclass when parent classes define them further up the inheritance tree?
To explain my question, I think this code sums up what I'm trying to achieve:
class Driver {}
class Formula1Driver extends Driver {
useFormula1Skills () {
console.log('Go go go!')
}
}
class Car {
public driver: Driver
constructor(driver: Driver) {
this.driver = driver;
}
}
class Formula1Car extends Car {
constructor(driver: Formula1Driver) {
super(driver)
this.driver.useFormula1Skills()
// TS Error: Property 'useFormula1Skills' does not exist on type 'Driver'.(2339)
}
}
Note the type error above.
A Car must have a Driver, but a Formula1Car must have a Formula1Driver and be able to call Formula1Driver-specific properties and methods.
I do not want Driver to define useFormula1Skills for obvious reasons, and figured that by stipulating that a Formula1Driver must be passed to the constructor in Formula1Car, that the type checker would allow my referencing of the subclass-specific method.
You can narrow down (override) the property in the subclass definition:
class Formula1Car extends Car {
constructor(public driver: Formula1Driver) {
super(driver)
this.driver.useFormula1Skills()
}
}
It wouldn't work the other way arround.
Note that above notation is equivalent to:
class Formula1Car extends Car {
public driver: Formula1Driver
constructor(driver: Formula1Driver) {
super(driver)
this.driver = driver;
...
}
}
The reason it works is that Car requires that the driver is Driver and F1Driver indeed is a Driver (note that since Driver does not have any properties, any object can be considered a Driver). When overriding properites, you can safely narrow them down - any kind of Driver can drive a Car, therefore F1Driver is okay to be type of driver in F1Car.
It is a design choice of typescript to be easier to work with but it indeed is supectible to runtime errors like in this case:
const vettel = new Formula1Driver();
const astonMartin = new Formula1Car(vettel);
const f1OnTheStreet: Car = astonMartin;
// UH OH!!!
f1OnTheStreet.driver = new Driver();
// remember that f1OnTheStreet is the same instance of F1
astonMartin.driver.useFormula1Skills();
// astonMartin.driver.useFormula1Skills is not a function
You cannot make a property that you want to access in the subclass private, but you can make it protected, meaning it cannot be accessed from the outside the class but can be access from a subclass. Note that modifiers must match. If you have a private property in Car, you cannot change (access) it in F1Car. If you have public in Car, you cannot make it private in F1Car.
class Car {
constructor(protected driver: Driver) {}
}
class F1Car extends Driver {
constructor(protected driver: F1Driver) {
super(driver);
}
}
This still allows Car to implement something like switchDrivers end end up with the same runtime error as above.
To make sure that nobody changes drivers, the property in Car needs to be readonly (and also in the subclasses
class Car {
constructor(public readonly driver: Driver) {}
}
Just note that these checks are only compile time so anything can happen if you access them without type checking.
I would suggest using generics, making the type of driver a type argument:
class Car<TDriver extends Driver> {
public driver: TDriver
constructor(driver: TDriver) {
this.driver = driver;
}
}
class Formula1Car extends Car<Formula1Driver> {
constructor(driver: Formula1Driver) {
super(driver)
this.driver.useFormula1Skills()
}
}

Workaround for accessing class type arguments in static method in Typescript

The following error
Static members cannot reference class type parameters.
results from the following piece of code
abstract class Resource<T> {
/* static methods */
public static list: T[] = [];
public async static fetch(): Promise<T[]> {
this.list = await service.get();
return this.list;
}
/* instance methods */
public save(): Promise<T> {
return service.post(this);
}
}
class Model extends Resource<Model> {
}
/* this is what I would like, but the because is not allowed because :
"Static members cannot reference class type parameters."
*/
const modelList = await Model.fetch() // inferred type would be Model[]
const availableInstances = Model.list // inferred type would be Model[]
const savedInstance = modelInstance.save() // inferred type would be Model
I think it is clear from this example what I'm trying to achieve. I want be able to call instance and static methods on my inheriting class and have the inheriting class itself as inferred type. I found the following workaround to get what I want:
interface Instantiable<T> {
new (...args: any[]): T;
}
interface ResourceType<T> extends Instantiable<T> {
list<U extends Resource>(this: ResourceType<U>): U[];
fetch<U extends Resource>(this: ResourceType<U>): Promise<U[]>;
}
const instanceLists: any = {} // some object that stores list with constructor.name as key
abstract class Resource {
/* static methods */
public static list<T extends Resource>(this: ResourceType<T>): T[] {
const constructorName = this.name;
return instanceLists[constructorName] // abusing any here, but it works :(
}
public async static fetch<T extends Resource>(this: ResourceType<T>): Promise<T[]> {
const result = await service.get()
store(result, instanceLists) // some fn that puts it in instanceLists
return result;
}
/* instance methods */
public save(): Promise<this> {
return service.post(this);
}
}
class Model extends Resource {
}
/* now inferred types are correct */
const modelList = await Model.fetch()
const availableInstances = Model.list
const savedInstance = modelInstance.save()
The problem that I have with this is that overriding static methods becomes really tedious. Doing the following:
class Model extends Resource {
public async static fetch(): Promise<Model[]> {
return super.fetch();
}
}
will result in an error because Model is no longer extending Resource correctly, because of the different signature. I can't think of a way to declare a fetch method without giving me errors, let alone having an intuitive easy way to overload.
The only work around I could get to work is the following:
class Model extends Resource {
public async static get(): Promise<Model[]> {
return super.fetch({ url: 'custom-url?query=params' }) as Promise<Model[]>;
}
}
In my opinion, this is not very nice.
Is there a way to override the fetch method without having to manually cast to Model and do tricks with generics?
You could do something like this:
function Resource<T>() {
abstract class Resource {
/* static methods */
public static list: T[] = [];
public static async fetch(): Promise<T[]> {
return null!;
}
/* instance methods */
public save(): Promise<T> {
return null!
}
}
return Resource;
}
In the above Resource is a generic function that returns a locally declared class. The returned class is not generic, so its static properties and methods have concrete types for T. You can extend it like this:
class Model extends Resource<Model>() {
// overloading should also work
public static async fetch(): Promise<Model[]> {
return super.fetch();
}
}
Everything has the types you expect:
Model.list; // Model[]
Model.fetch(); // Promise<Model[]>
new Model().save(); // Promise<Model>
So that might work for you.
The only caveats I can see right now:
There's a bit of duplication in class X extends Resource<X>() which is less than perfect, but I don't think you can get contextual typing to allow the second X to be inferred.
Locally-declared types tend not to be exportable or used as declarations, so you might need to be careful there or come up with workarounds (e.g., export some structurally-identical or structurally-close-enough type and declare that Resource is that type?).
Anyway hope that helps. Good luck!

Why method added through decoration is not accessible

Suppose that I have a Directive decorator which adds a static method to it's target called factory:
function Directive<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
static factory(...args): ng.IDirectiveFactory {
const c: any = () => {
return constructor.apply(this, args);
};
c.prototype = constructor.prototype;
return new c(...args);
}
};
}
I also add the type via an interface:
interface BaseDirective extends ng.IDirective {
factory(): ng.IDirectiveFactory;
}
Why in my class declaration of:
#Directive
class FocusedDirective implements BaseDirective {....
I get a Class 'FocusedDirective' incorrectly implements interface 'BaseDirective'.
Property 'factory' is missing in type 'FocusedDirective'.
Am I wrong to expect from #Directive to add this missing property for me?
Decorators can't change the type of the class, you can invoke your decorator as a function and store the new class which will contain the method and use the new class instead of the original:
const FocusedDirectiveWithDirective = Directive(FocusedDirective);
You can do away with the intermediate class altogether by using class expressions:
const FocusedDirective = Directive(class implements BaseDirective{
});
You have two problems. The first has little to do with decorators: factory is a static method in your implementation, but a regular method in your interface:
interface BaseDirective {
factory(): ng.IDirectiveFactory;
}
That's going to be a problem for you. For now I'm going to convert the implementation to a regular method, since it's simpler to implement.
function Directive<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
factory(...args: any[]): ng.IDirectiveFactory {
const c: any = () => {
return constructor.apply(this, args);
};
c.prototype = constructor.prototype;
return new c(...args);
}
};
}
The second issue: decorators do not mutate the class signature the way you're expecting. This is an oft-requested feature and there are some interesting issues around why it's not a simple problem to solve. Importantly, it's not easy to figure out how to support having the implementation of the class refer to the mutated type. In your case: would the stuff inside your {.... know about factory() or not? Most people seem to expect it would, but the decorator hasn't been applied yet.
The workaround is not to use decorator syntax at all, but instead to use the decorator function as a regular function to create a new class. The syntax looks like this:
class FocusedDirective extends Directive(class {
// any impl that doesn't rely on factory
prop: string = "hey";
foo() {
this.prop; // okay
// this.factory(); // not okay
}
}) implements BaseDirective {
// any impl that relies on factory
bar() {
this.prop; // okay
this.foo(); // okay
this.factory(); // okay
}
}
This also solves the "does the implementation know about the decorator" issue, since the stuff inside the decorator function does not, and the stuff outside does, as you see above.
Back to the static/instance issue. If you want to enforce a constraint on the static side of a class, you can't do it by having the class implement anything. Instead, you need to enforce the static side on the class constructor itself. Like this:
interface BaseDirective {
// any actual instance stuff here
}
interface BaseDirectiveConstructor {
new(...args: any[]): BaseDirective;
factory(): ng.IDirectiveFactory;
}
class FocusedDirective extends Directive(class {
// impl without BaseDirectiveConstructor
}) implements BaseDirective {
// impl with BaseDirectiveConstructor
bar() {
FocusedDirective.factory(); // okay
}
}
function ensureBaseDirectiveConstructor<T extends BaseDirectiveConstructor>(t: T): void {}
ensureBaseDirectiveConstructor(FocusedDirective);
The ensureBaseDirectiveConstructor() function makes sure that the FocusedDirective class constructor has a static factory() method of the right type. That's where you'd see an error if you failed to implement the static side.
Okay, hope that helps. Good luck.

Proxy cannot get method in extended class

I am trying to use a Proxy, and I am having issues. I have a class like so:
export class Builder {
public doSomething(...args: (string | number | Raw | Object)[]): this {
// Do stuff
return this
}
}
export class ModelBase extends Builder {
protected _items = {}
}
export class Model extends ModelBase {
public constructor(options?: ModelSettings) {
super(options)
return new Proxy(this, {
get: function (target, property) {
return target._items[property] || target
}
})
}
public static create() {
return new this()
}
}
I then extend Model like so:
export class MyClass extends Model {
public constructor() {
super({/* Some options go here */})
// Do some stuff
}
public static getItems() {
let t = this.create()
t.doSomething()
}
}
Then I call getItems() which creates an instance of the class.
MyClass.getItems()
When I run this, I get the error:
TypeError: t.doSomething is not a function
Where doSomething() is within the class ModelBase. If I comment out the Proxy things work as usual. So, I would like to know why I can't access the parent class.
Your proxy is trying to find target._items.doSomething, but that doesn't exist. target.doSomething does exist, but it's not looking for that. As a result, it falls back to returning target, which is the object as a whole. The object, is not a function, and thus the error.
As for how to fix this, that depends on what you're trying to achieve with the proxy. If there's a limited set of properties that the proxy should be paying attention to, you could check those explicitly. Or more generally you might be able to check whether target[property] exists, then fall back to target._items[property] and only then fall back to target. Again, it depends on what you're trying to achieve.

Categories

Resources