Issue on Typescript Conditional Types with "this" type in class context

Viewed 260

i'm trying to use conditional types with 'this' Type in class context.

i summarized my issues, on those next examples:


type Foo<T> = T extends string ? T : never;

// 'Foo' conditional type don't pass with 'this' Type
class A {
    public val: string = 'bar'
    public getOtherVal(): Foo<this['val']> {
        return 'baz' // error: Type 'string' is not assignable to type 'Foo<this["val"]>'.
    }
}

// 'Foo' conditional type pass with class Type (here 'B')
class B {
    public val: string = 'bar'
    public getOtherVal(): Foo<B['val']> {
       return 'baz'
    }
}

// Typing with 'this' Type and without the conditional Type pass
class C {
    public val: string = 'bar'
    public getOtherVal(): this['val'] {
        return 'baz'
    }
}

The examples on the typescript playground => here

I actually don't understand why the first one don't pass whit "this" type ? This type is really convenient when you have to define abstracts classes but this issue globally blocking its usage with Conditional types... maybe i miss something but i'm open to some insights to fix it or other practices ?

thx for your help !


+++ Edit +++ to be more accurate on the issue:

The main idea in my context is to use This type to not fix Class type, That allow to add some constraint on some abstract methods or property of class, depending on its implementations.

I illustrated the issue in an other example :

type Foo<T> = T extends string ? T : (number | boolean);

// Not pass with conditional type:

// Abstract declaration
abstract class A {
    public abstract val: string | number | boolean
    // function return a value depending of class property value.
    public abstract getOtherVal(): Foo<this['val']>
}

// Implementation of A class
class Aa implements A {
    public val: string = 'bar';
    // Return type constraint by the type of val.
    public getOtherVal() { // error: ...Type 'string' is not assignable to type 'Foo<this["val"]>'.
        return 'baz'
    }
}

// Pass without conditional type:

// Abstract declaration
abstract class B {
    public abstract val: string | number | boolean
    // function return a value depending of class property value.
    public abstract getOtherVal(): this['val']
}

// Implementation of B class
class Bb implements B {
    public val: string = 'bar';
    // Return type constraint by the type of val.
    public getOtherVal() {
        return 'baz'
    }
}

On Typescript playground => here

2 Answers

This is a proper behavior. At least when it refers to classes A and B in Your example. Actually it is the class C which behaves improperly. Let me explain.

First, (from the official docs) the polymorphic this type represents a type that is the subtype of the containing class or interface.

Consider the following:

// Your original class C
class C {
    public val: string = 'bar'
    public getOtherVal(): this['val'] {
          return 'baz' // no warning, no error!
    }
}

class C1 extends C {
    val: 'a' | 'b'; 
    // perfectly OK, as 'a' | 'b' extends string 
    // (and the type of val field in superclass C was string).
}

class C2 extends C {
    val: number;
    // produces Error: 
    //    Property 'val' in type 'C2' is not assignable to the same property in base type 'C'. 
    //    Type 'number' is not assignable to type 'string'.ts(2416)`.
}

const val = new C1().getOtherVal();
// val constant is of type 'a' | 'b', not string. But actually it holds string 'baz'.

Type of this[‘val’] may be any subtype of string. So it should not be allowed to return string when this[‘val’] is expected - as this may be restricted in subclasses. This is what class A does and it is perfectly OK. Contrary, The class C allows to return string where type this[‘val’] is expected. So this is class C which behaves improperly not class A.

Generally speaking, this as return type allows you to return type of subclass of this from inherited methods without any modification. It is not an abbreviation for the long class name!

class Fluent {
    doSomething(): this { /* ... */ return this; }
    doSomethingElse(): this { /* ... */ return this; }
}

class Subclass extends Fluent {
    doInSubclass(): this { /* ... */ return this; }
};

const instance: Subclass = new Subclass().doSomething().doInSubclass().doSomethingElse();

I dont know why the Class B's implementation isn't good enough for you... but doesn't InstanceType solve your problem?

// 'Foo' conditional type don't pass with 'this' Type
class A {
    public val: string = 'bar'
    public getOtherVal(): Foo<InstanceType<typeof A>['val']> {
        return 'baz'
    }
}

but honestly, if Im doing something like this, I would just rather have it organized as:

type Foo<T> = T extends string ? T : never;

type Value = string;

class A {
    public val: Value = 'bar'
    public getOtherVal(): Foo<Value> {
        return 'baz'
    }
}
Related