Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method overriding in Typescript with different return types without hack

I am trying to use a class adapter pattern in typescript

class Source {
    getSomething(): TypeA
}

class Adapter extends Source {
   getSomething(): TypeB {
     const response = super.doSomething();
     // do something with the shape of response
     return response;
   }
}

Typescript is complaining that these two types are incompatible. I am not sure if this is something doable in Typescript without typehint any or union TypeA | TypeB. Imagine a scenario where I want the client code to be able to use both the Source and Adapter class and still be able to get proper type checks.

Thanks

like image 282
Raheel Avatar asked Feb 27 '26 11:02

Raheel


1 Answers

The real reason this is not possible is that since Adapter extends Source, any instance of Adapter should be assignable to a reference of Source, and not cause any runtime issues. If TypeA and TypeB are unrelated, you can have runtime errors from such an assignment:

class TypeA { a() { return 0 } }
class Source {
    getSomething(): TypeA {
        return null!
    }
}
class TypeB { b() { return 0 } }
class Adapter extends Source {
   getSomething(): TypeB {
     return null!
   }
}

let source: Source = new Adapter();
source.getSomething().a() // error a is not defined on TypeB which is what  Adapter actually returns 

Playground Link

If TypeB extends TypeA (either class inheritance or structural subtyping) then typescript actually does not complain and allows your definitions:

class TypeA { a() { return 0 } }
class Source {
    getSomething(): TypeA {
        return null!
    }
}
class TypeB extends TypeA { b() { return 0 } }
class Adapter extends Source {
   getSomething(): TypeB {
     return null!
   }
}

let source: Source = new Adapter();
source.getSomething().a() // This is ok now

Playground Link

Now to get the compiler to accept your code, you can make Source generic (with the generic having a default of TypeA), then you would get correct types and only get errors if you try to assign an Adapter to a Source which is what would cause the runtime error:

class TypeA { a() { return 0 } }
class Source<T = TypeA> {
    getSomething(): T {
        return null!
    }
}
class TypeB  { b() { return 0 } }
class Adapter extends Source<TypeB> {
   getSomething(): TypeB {
     return null!
   }
}

let adapter =  new Adapter()
let source: Source = adapter; // this is an error
adapter.getSomething().b() // this is fine and returns TypeB

Playground Link

If Source is not within your control to change, you could perform some type surgery on it to get it to work by omitting the offending method, but I would not recommend it.

class TypeA { a() { return 0 } }
class Source {
    getSomething(): TypeA {
        return null!
    }
}
class TypeB  { b() { return 0 } }
const ModifiedSource = Source as new(...p: ConstructorParameters<typeof Source>) => Omit<Source, 'getSomething'>
class Adapter extends ModifiedSource {
   getSomething(): TypeB {
     return null!
   }
}

let adapter =  new Adapter()
let source: Source = adapter; // this is still an error
adapter.getSomething().b() // this is fine and returns TypeB

Playground Link

like image 128
Titian Cernicova-Dragomir Avatar answered Mar 02 '26 00:03

Titian Cernicova-Dragomir