This TypeScript error occurs when overriding a method in a subclass where the return type is not compatible with the base class method's return type. It's a type safety check that ensures method overrides maintain type compatibility, preventing runtime errors from incompatible return types. Understanding covariance and type compatibility rules resolves this object-oriented programming issue.
This error occurs when you override a method in a subclass and the return type you specify is not compatible with the return type declared in the base class. TypeScript enforces that method overrides must be type-safe: the return type in the subclass must be assignable to (compatible with) the return type in the base class. This is known as covariance for return types. The error prevents situations where code expecting the base class return type receives an incompatible type from the subclass method, which could cause runtime errors. For example, if a base class method returns `Animal`, a subclass method can return `Dog` (since Dog is a subtype of Animal), but not `Vehicle` (which is unrelated). This ensures the Liskov Substitution Principle is maintained in your object-oriented design.
First, examine the base class or interface to understand what return type is expected. The error message shows the expected type (X). You need to ensure your subclass method returns a type that is assignable to this type.
class Animal {
makeSound(): string {
return "generic animal sound";
}
}
class Dog extends Animal {
// This must return something assignable to string
makeSound(): string {
return "bark";
}
}The subclass method can return a more specific type (subtype) but not a completely different type. This is covariance: you can return a Dog where Animal is expected, but not a Car.
class Animal {}
class Dog extends Animal {}
class Car {}
class AnimalHandler {
getAnimal(): Animal {
return new Animal();
}
}
class DogHandler extends AnimalHandler {
// OK: Dog is a subtype of Animal
getAnimal(): Dog {
return new Dog();
}
}
class CarHandler extends AnimalHandler {
// ERROR: Car is not a subtype of Animal
getAnimal(): Car {
return new Car();
}
}If the base class returns a union type, the subclass must return a type that is assignable to that union. The subclass can return a subset of the union or a single type from the union.
class Base {
getValue(): string | number {
return "hello";
}
}
class Sub1 extends Base {
// OK: string is assignable to string | number
getValue(): string {
return "subclass value";
}
}
class Sub2 extends Base {
// OK: number is assignable to string | number
getValue(): number {
return 42;
}
}
class Sub3 extends Base {
// ERROR: boolean is not assignable to string | number
getValue(): boolean {
return true;
}
}When overriding generic methods, ensure type parameters are compatible. The subclass can use the same or more specific type parameters, but not completely different ones.
class Base<T> {
process(value: T): T {
return value;
}
}
class Sub1 extends Base<string> {
// OK: same type parameter
process(value: string): string {
return value.toUpperCase();
}
}
class Sub2 extends Base<Animal> {
// OK: returning Dog (subtype of Animal) where Animal is expected
process(value: Animal): Dog {
return new Dog();
}
}
class Sub3 extends Base<number> {
// ERROR: string is not assignable to number
process(value: number): string {
return value.toString();
}
}Sometimes you need to widen the return type in the subclass to match the base class. This might mean returning a less specific type than you'd prefer, but it maintains type safety.
interface Shape {
area(): number;
}
class Circle implements Shape {
radius: number;
// Must return number to satisfy Shape interface
area(): number {
return Math.PI * this.radius * this.radius;
}
}
// If you try to return something more specific:
class Square implements Shape {
side: number;
// ERROR: {value: number, unit: string} is not assignable to number
area(): {value: number, unit: string} {
return {value: this.side * this.side, unit: "cm²"};
}
// CORRECT: return number
area(): number {
return this.side * this.side;
}
}When implementing an interface, ensure your method return types match exactly or are subtypes of the interface return types.
interface Repository<T> {
findById(id: string): T | null;
}
class UserRepository implements Repository<User> {
// OK: User | null is assignable to User | null
findById(id: string): User | null {
// implementation
return null;
}
}
class ProductRepository implements Repository<Product> {
// ERROR: Product is not assignable to Product | null
// (missing null in return type)
findById(id: string): Product {
// implementation
return new Product();
}
// CORRECT: include null
findById(id: string): Product | null {
// implementation
return new Product();
}
}If you need to return different types based on conditions, consider using method overloads. However, the implementation signature must be compatible with all overloads.
abstract class Parser {
abstract parse(input: string): any;
}
class JsonParser extends Parser {
// Using overloads for more specific return types
parse(input: string): object;
parse(input: string): any {
try {
return JSON.parse(input);
} catch {
return null;
}
}
}
// The implementation returns any, which is assignable to any
// The overload provides more specific type information to callersSome TypeScript compiler flags affect method override checking. The strict flag enables strict checking of method overrides. If you're migrating from JavaScript or a less strict TypeScript configuration, you might need to adjust your code.
{
"compilerOptions": {
"strict": true, // Enables strict method override checking
"noImplicitOverride": true // Requires explicit `override` keyword
}
}With noImplicitOverride, you must use the override keyword:
class Base {
method(): string {
return "base";
}
}
class Derived extends Base {
override method(): string {
return "derived";
}
}TypeScript uses structural typing for most type compatibility, but for method overrides it follows specific rules. Return types are covariant: a subclass method can return a subtype of the base class return type. This is different from parameter types, which are contravariant (when strictFunctionTypes is enabled). The override keyword (available in TypeScript 4.3+) helps catch errors by requiring explicit annotation of method overrides. When working with generic classes, remember that class Sub extends Base<SpecificType> creates a new class hierarchy; the subclass methods must be compatible with the instantiated base class. For abstract classes, concrete subclasses must implement all abstract methods with compatible return types. When using conditional types like T extends U ? X : Y in return types, ensure all possible branches are assignable to the base return type. The ReturnType<T> utility type can help extract return types for comparison. Remember that void is a special return type: a method returning void can be overridden by a method returning any type (except undefined with strict settings), but the return value will be ignored.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript