0

Per my understanding of https://www.typescriptlang.org/docs/handbook/2/classes.html#this-types I should be able to use this as a type in place of the name of the base class to allow inheriting classes to sub in their class type.

I am trying to use this to create a method that takes an object which has the same properties as the class it belongs to, and uses them to create a modified copy:

type ClassProperties<C> = {
  [K in keyof C as C[K] extends Function ? never : K]: C[K]
}

abstract class BaseClass {

  clone() { return structuredClone(this); }

  toModified(change: Partial<ClassProperties<this>>) {
    const c = this.clone();
    type changetype = typeof change;
    type changekeys = keyof changetype;
    type changevals = changetype[changekeys];
    Object.entries(change).forEach(([k, v]: [changekeys, changevals]) => { (c[k] as changevals) = v; });
    return c;
  }
}

export class ConcreteClass extends BaseClass {
  constructor(public a: number, public b: string = "", public c: boolean = false) {
    super();
  }
}

This gives the following error: typescript [2345]: Argument of type '([k, v]: [keyof ClassProperties<this>, changevals]) => void' is not assignable to parameter of type '(value: [string, unknown], index: number, array: [string, unknown][]) => void'.

If I write out an impl for the inheriting class, it works:

export class ConcreteClass extends BaseClass {
  constructor(public a: number, public b: string = "", public c: boolean = false) {
    super();
  }

  toModified(change: Partial<ClassProperties<ConcreteClass>>) {
    const c = this.clone();
    type changetype = typeof change;
    type changekeys = keyof changetype;
    type changevals = changetype[changekeys];
    Object.entries(change).forEach(([k, v]: [changekeys, changevals]) => { (c[k] as changevals) = v; });
    return c;
  }
}

But since I have several classes that inherit from the base class, I'd really like to avoid the duplication.

Camden Narzt
  • 2,271
  • 1
  • 23
  • 42
  • `Object.entries()` is not strongly typed [for various reasons](https://stackoverflow.com/a/62055863/2887218) and that's responsible for what you're seeing. I'd just use a type assertion on `c` like [this playground link](https://tsplay.dev/w1akKw) shows and move on. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? – jcalz Aug 25 '23 at 00:20
  • That doesn't help with the function signature, which in the concrete case limits the passed in change object to properties that exist on the concrete class. – Camden Narzt Aug 25 '23 at 17:12
  • I don't understand; could you show a piece of code that demonstrates the problem? Maybe with a playground link? [If I look at this](https://tsplay.dev/N9VVVm) it seems to be what I think you're looking for, but maybe I'm misunderstanding? – jcalz Aug 26 '23 at 01:02
  • sure [here's](https://tsplay.dev/w6VVew) a simplified example that shows the `toModified` method not typechecking when the `this` variable is confirmed to be a subclass. – Camden Narzt Aug 26 '23 at 04:25
  • That looks like the next issue you'd run into after resolving this one, and is thus out of scope for the question as asked. There's a general limitation with `this` that it's treated as an implicit generic type parameter inside class bodies, so any complicated type operation on it will end up confusing the compiler. If you want to work around that you can widen `this & ConcreteClass` to `ConcreteClass` as shown [in this playground link](//tsplay.dev/WoMMgN), but exploring this should be in another question. If you agree I'll write my original suggestion as an answer; if not, why? – jcalz Aug 26 '23 at 13:02

2 Answers2

0

What you seem to be expecting is that because this works in the case this = ConcreteClass that your program should typecheck. However, TypeScript expects the types to agree universally, which means it is not going to assume any particular subclass.

The full error is:

Argument of type '([k, v]: [keyof ClassProperties<this>, changevals]) => void' is not assignable to parameter of type '(value: [string, unknown], index: number, array: [string, unknown][]) => void'.
  Types of parameters '__0' and 'value' are incompatible.
    Type '[string, unknown]' is not assignable to type '[keyof ClassProperties<this>, changevals]'.
      Type at position 0 in source is not compatible with type at position 0 in target.
        Type 'string' is not assignable to type 'keyof ClassProperties<this>'.
          Type 'string' is not assignable to type '(this[K] extends Function ? never : K) | (this["clone"] extends Function ? never : "clone") | (this["toModified"] extends Function ? never : "toModified")'.

What is happening is that [string, unknown] is the expected argument type, but you're passing a function with argument type [keyof ClassProperties<this>, changevals], and these are not compatible. For example, keyof ClassProperties<this> would have to be a supertype of (or equal to) string. TypeScript cannot reduce keyof ClassProperties<this> any further because this is just a type variable. It isn't any particular class.

erisco
  • 14,154
  • 2
  • 40
  • 45
0

The best I've been able to do is write the impl in the base class:

  protected _toModified(change: object) {
    const c = this.clone() as any;
    Object.entries(change).forEach(([k, v]) => { c[k] = v; });
    return c;
  }

and then use it in the concrete class:

  toModified(change: Partial<ClassProperties<ConcreteClass>>) {
    return this._toModified(change);
  }
Camden Narzt
  • 2,271
  • 1
  • 23
  • 42