5

I have a class with some property/methods that return value which was orignially passed via the constructor:

class Base {
  constructor(arg) {
     this.options = arg.options;
  }
}

People suggest this class with generics as the typescript equivalent:

class Base<T extends {options: any}> {
    options: T["options"];

    constructor(arg: T) {
        this.options = arg.options;
    }    
}

// works fine with instantiation
const inst = new Base({options: {foo: 123}});
inst.options.foo++; // ok
inst.options.bar; // TS Error

Now i have a sub class which calls the super class's constructor and passes these options:

class Sub extends Base { // TS Error
    constructor() {
        super({options: {foo: 123}});
        this.options.foo++; // TS Error
    }
}

Typescript compiler isn't happy with this and wants the Base class being extended passing the type. But then i have the information twice, in the super call and the extends notation. What is the correct ts syntax to solve my issue?

(code in typescript playground)

mbehzad
  • 3,758
  • 3
  • 22
  • 29
  • Your playground link is chopped off, so the code cuts out. Is it just the code in the question? If so, I've fixed it. If not, please add the code both to the playground and the question. :-) – T.J. Crowder Feb 28 '21 at 11:03
  • Is the value you pass `super` really a compile-time literal as it is in your question? – T.J. Crowder Feb 28 '21 at 11:07
  • FWIW, I doubt you can make TypeScript infer the type argument for `Base` from the `super` call. I suspect the near-duplication of `{options: {foo: number}}` and `{options: {foo: 123}}` is unavoidable. – T.J. Crowder Feb 28 '21 at 11:09
  • @T.J.Crowder, i fixed the link. yes the value is fixed in the code and won't be read / build in the runtime. it actually defines what features this particular class has, and the super class provides this argument in the ctor to generate these features on the fly. (if that makes sense) somehow TS is able to extract some type information when the Base class is called via the `new` keyword. so i hoped same logic can work when extending the class. but it only needs some changes to the ts code. – mbehzad Feb 28 '21 at 11:39

1 Answers1

1

Edit:

The problem doesn't lie with super() but with extends Base. One way or another, typescript has to infer the generic type for Base and you can't expect typescript to infer that from the call to super().


I would have at first suggested doing this

    class Sub<T extends {options: any}> extends Base<T> {
    constructor() {
        super({options: {foo: 123}})
    }}

but this won't work since a concrete instance is not allowed to be assigned to a type parameter. It would have worked if we would instead have done this

    class Sub<T extends {options: any}> extends Base<T> {
    constructor(arg: T) {
        super(arg)
    }}

const inst2 = new Sub({options: {foo: 123}})
inst2.options.foo++; // ok   

I think the correct solution would be to simply do this

    class Sub extends Base<{options: any}> {
    constructor() {
        super({options: {foo: 123}})
    }}

However, there's still a problem.

const inst2 = new Sub();
inst2.options.foo++; //options is any which makes sense, we said that options is any above

Do you know why we use generics? When we don't know in advance what the type is. Since in this instance we know exactly what the type is, then we can safely do this:

class Sub extends Base<{ options: {foo: number} }> {
    // or even class Sub extends Base<{ options: {foo: 123} }> {
    constructor() {
        super({options: {foo: 123}});
        this.options.foo++;
    }
}

const inst2 = new Sub();
inst2.options.foo++;

Is it okay to do this? Of course, if we passed something other than options to Base, it would have triggered a warning. So we meet the requirements of Base and at the same time make sure that what we pass to super() are in accordance with what we said the generic type is.

jperl
  • 4,662
  • 2
  • 16
  • 29