Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using "this" parameter in Typescript static methods doesn't narrow the type to the current class?

This is best explained using an example. I need to reference the current class in a static method, this works as expected:

class Cls {
  static fn<T extends typeof Cls>(
    this: T,
    arg: T extends typeof Cls ? true : false,
  ) {}
}

Cls.fn(true);
Cls.fn(false); // Argument of type 'false' is not assignable to parameter of type 'true'.

However, when calling Cls.fn from another static method, it doesn't work:

class Cls {
  static fn1<T extends typeof Cls>(
    this: T,
    arg: T extends typeof Cls ? true : false,
  ) {}

  static fn2<T extends typeof Cls>(this: T) {
    this.fn1(true); // Argument of type 'true' is not assignable to parameter of type 'T extends typeof Cls ? true : false'.
  }
}

TS Playground

Interestingly, it works if I remove fn2's generic or do this.fn1<typeof Cls>(true). This means the error is related to the type of this. I think it has to do with this referring to any subclass of Cls as opposed to exactly Cls. However, even if this is a subclass of Cls, T extends typeof Cls would still be true.

Is this a bug where Typescript uses the wrong value for this? If it's not a bug, how can I fix it?

In my actual code, I need to reference the current class because the method accepts different arguments depending on the subclass.

Edit: here's a more realistic example using subclasses

like image 528
Leo Jiang Avatar asked May 25 '26 10:05

Leo Jiang


1 Answers

The behavior is explained by an unresolved generic type parameter.

This is caused by an indirection introduced by what is basically a higher-order function (method). In the case of a non-generic method fn2 (using the Sub/Base example), the inference works as expected as the type parameter gets resolved to typeof Sub in the derived class Sub:

Base.fn<typeof Sub>(this: typeof Sub, arg: "sub"): void

Unfortunately, in the case of fn3, it is itself a generic function (method), leading to the T generic parameter in calls to fn being unresolved, which can be seen from the inferred signature:

Base.fn<T>(this: T, arg: T extends typeof Sub ? "sub" : "base"): void

This clears up what the following compiler error means - as the conditional type is also left unresolved, neither "sub" nor "base" will be assignable to T extends typeof Sub ? "sub" : "base":

Argument of type '"sub"' is not assignable to parameter of type 'T extends typeof Sub ? "sub" : "base"'

One could, as you rightfully noted, remove the generic type parameter of the higher-order method, thus removing the indirection:

static fn3(this: typeof Sub) {
    this.fn('base'); // error
    this.fn('sub'); // OK
}

This, however, presents complications in further derived classes should you ever need this:

class Sub {   
    static fn4(this: typeof Sub) {
        this.fn('sub');
        return this;
    } 
}

class SubSub extends Sub {}

SubSub.fn4(); // typeof Sub, probably wanted typeof SubSub

There is an alternative, though - do not constrain the T parameter and instead constrain the type of this based on what T is inferred to be. The classic technique is using a conditional type that resolves to itself of never:

class Sub extends Base {
  static fn3<T>(this: T extends typeof Sub? T: never) {
    this.fn('base'); // error
    this.fn('sub'); // OK
    return this;
  }
}

Sub.fn('sub'); // OK
Sub.fn('base'); // error
Sub.fn3(); // typeof Sub

The signature of the inner this.fn() call is inferred as:

Base.fn<T extends typeof Sub ? T : never>(this: T extends typeof Sub ? T : never, arg: (T extends typeof Sub ? T : never) extends typeof Sub ? "sub" : "base"): void

By further deferring the evaluation via T extends typeof Sub ? T : never we actually helped the compiler: now it nows that T is guaranteed to be typeof Sub by the time of instantiation.

Playground

like image 124
Oleg Valter is with Ukraine Avatar answered May 28 '26 01:05

Oleg Valter is with Ukraine



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!