TypeScript throws TS2416 when a subclass method or property has a type that is incompatible with the parent class or interface. The message looks like this:
Property 'greet' in type 'Child' is not assignable to the same property in base type 'Parent'. Type '(name: number) => void' is not assignable to type '(name: string) => void'. ts(2416)
This is a Liskov Substitution Principle check: a subclass must be usable anywhere its parent is expected. Here is why it fires and four ways to fix it.
The simplest reproducer
class Parent {
greet(name: string): void {
console.log(`Hello, ${name}`);
}
}
class Child extends Parent {
greet(name: number): void { // TS2416: param types differ
console.log(`Hello, ${name}`);
}
}Any code that has a Parent reference expects to call greet(someString). If Child is passed instead, that call breaks. TypeScript blocks the mismatch at compile time.
Fix 1: Match the parent’s signature exactly
The right fix in most cases. If you meant to override, keep the same signature:
class Child extends Parent {
greet(name: string): void { // matches parent
console.log(`Hi from Child, ${name}`);
}
}Overriding the implementation is fine. Overriding the shape is not.
Fix 2: Widen the parent (contravariant parameters)
If Child accepts a broader type than Parent, that is safe. Parent expected a string, Child accepts string or number, so any string caller still works:
class Child extends Parent {
greet(name: string | number): void { // wider than parent, OK
console.log(`Hello, ${name}`);
}
}Note: TypeScript’s strictFunctionTypes setting affects this. With strict mode on, methods are checked bivariantly (both directions), so a wider Child parameter may still be flagged. If it is, use fix 3 or 4 instead.
Fix 3: Narrow the return (covariant returns)
Return types work the opposite way. Child can return a narrower type than Parent:
class Animal {
clone(): Animal {
return new Animal();
}
}
class Dog extends Animal {
clone(): Dog { // narrower return, OK
return new Dog();
}
}This is safe because any code expecting Animal can accept Dog (Dog is an Animal). Common in factory methods and builder patterns.
Fix 4: Use generics on the base class
If different subclasses genuinely need different property types, make the base class generic:
class Container<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
class StringContainer extends Container<string> {
constructor(value: string) {
super(value);
}
}
class NumberContainer extends Container<number> {
constructor(value: number) {
super(value);
}
}Each subclass pins the generic to a concrete type. No TS2416 because both are legitimate instantiations of the same parent shape.
Common places TS2416 shows up in real projects
- Overriding React lifecycle methods with different signatures. componentDidUpdate and shouldComponentUpdate must match the base React.Component signature exactly.
- Extending Express Router or Application classes. If you subclass an Express class and change a method signature, TS2416 fires.
- Interface implementation with wrong types. The same error fires when a class implements an interface but the property types do not match.
- Type-narrowed subclasses that need different fields. Discriminated union types often fit better than class hierarchy here.
- Migrating class-based code from JavaScript. Legacy code often has loose method signatures that TypeScript now flags.
Should you use inheritance at all?
TS2416 is often a hint that inheritance is the wrong tool. Consider composition instead:
// Instead of Child extends Parent with a mismatched greet:
class Greeter {
greet(name: string): void { ... }
}
class Child {
private greeter = new Greeter();
greetNumber(id: number): void {
this.greeter.greet(String(id));
}
}Composition sidesteps the whole hierarchy question. Two objects with two different jobs. If you find yourself fighting TS2416 repeatedly on the same class tree, this is probably the better refactor.
Which fix should you pick?
- You accidentally changed the signature: Fix 1 (match parent exactly).
- Child truly accepts a broader input: Fix 2 (widen the parameter), if strictFunctionTypes allows.
- Child returns a more specific type: Fix 3 (narrow the return), always safe.
- Different subclasses need different concrete types: Fix 4 (generics on base).
- Inheritance itself is fighting you: skip the fix and refactor to composition.
Verifying your fix
npx tsc --noEmit
If TS2416 no longer appears, your fix is in place. Also add the override keyword to intentional overrides so future accidental signature drift gets a clearer error message.
Official documentation
Frequently asked questions
What is the difference between TS2416 and TS2611?
TS2416 fires when a property or method type conflicts with the base. TS2611 is a related error about class members that should be readonly. Both catch inheritance mistakes but at different levels. Fix each individually with the right technique for the case.
Should I always use the override keyword?
Yes, if you have noImplicitOverride enabled in tsconfig. The keyword makes intentional overrides explicit and catches typos in method names (which would otherwise silently create a new method instead of overriding).
Can I use TS2416 to enforce a design pattern?
Yes. If you want subclasses to strictly follow a contract, define the parent methods with specific types. TS2416 will fire on any subclass that deviates, forcing the developer to conform to the interface.
Does TS2416 fire for interface implementation?
A similar error (TS2416 or TS2611) fires when a class implements an interface with the wrong method types. The fixes are the same: match the interface signature exactly, or make the interface generic if different classes need different types.
Why does TypeScript allow narrower return but not narrower parameter?
Because a caller of the base class expects to pass any valid input and receive any valid output. If the subclass demands a narrower input, some legitimate base-class calls would break. If the subclass returns a narrower output, all callers still handle it fine. This is called Liskov substitution.
Should I switch from inheritance to composition to avoid TS2416?
Often yes. TS2416 sometimes signals that the class hierarchy is not the right shape for the problem. Composition (has-a) is more flexible than inheritance (is-a) for most modern TypeScript codebases. Refactor to composition when you find yourself repeatedly fighting inheritance rules.
