TS2416Semantic Error
Since TS 1.0Updated in TS 2.7

Fix TS2416: Property Not Assignable to Base Type

Learn why TypeScript throws TS2416 when a subclass member clashes with the base type, and how to fix widened properties, overrides and React state.

error TS2416: Property 'X' in type 'Y' is not assignable to the same property in base type 'Z'

What This Error Means

TypeScript error TS2416 means a class declared a member that does not fit the member of the same name in the type it inherits from. That base type may be a class you extends or an interface you implements — either way the compiler checks every member of the derived type against the base and complains about each one that does not line up.

The rule the checker applies is plain assignability: the derived member's type must be assignable to the base member's type. Subclassing is a promise that instances of the child can be used anywhere the parent is expected, so a property cannot become wider, a method cannot return something the caller was not told to expect, and an override cannot demand more from its arguments than the base signature allows.

// The general shape of the error:
// Property 'channel' in type 'PushNotification' is not assignable to the same property in base type 'UserNotification'.
//           ~~~~~~~            ~~~~~~~~~~~~~~~~                                                  ~~~~~~~~~~~~~~~~
//           the member         your class                                                        what it must match

The error always comes with an indented elaboration underneath that names the two concrete types — read it before anything else, because that second line is where the actual mismatch is spelled out. Since TypeScript 2.7 each offending member gets its own TS2416; before that, the compiler reported one class-level message instead, which is why older Stack Overflow answers talk about "Class 'X' incorrectly extends base class 'Y'" (today TS2415).

Common Causes

1. Widening a Property's Type in the Subclass

The base narrows a property to a union of literals, and the subclass re-declares it as the general type. From the compiler's point of view the child just broke the parent's guarantee: anyone holding a UserNotification may assume channel is one of two strings.

// ❌ Broken
class UserNotification {
  channel: 'email' | 'sms' = 'email'
}
 
class PushNotification extends UserNotification {
  channel: string = 'push'
  // ~~~~~~~ Error: Property 'channel' in type 'PushNotification' is not assignable to the same property in base type 'UserNotification'.
  //         Type 'string' is not assignable to type '"email" | "sms"'.
}

The fix belongs in the base class, not the child. Widen the union once where the contract lives, then let the subclass pin itself to a single member of it:

// ✅ Fixed — the base type is wide enough, the subclass narrows within it
class UserNotification {
  channel: 'email' | 'sms' | 'push' = 'email'
}
 
class PushNotification extends UserNotification {
  channel = 'push' as const
}

The as const matters: without it the initializer is inferred as string and you are back at the same error.

2. An Override Whose Return Type Does Not Match

Abstract repositories, toJSON, and framework lifecycle hooks all fail this way. Here the base promises a Promise, and the implementation forgets to be asynchronous.

// ❌ Broken
interface User {
  id: string
  name: string
}
 
abstract class Repository<T> {
  abstract find(id: string): Promise<T | null>
}
 
class UserRepo extends Repository<User> {
  private cache = new Map<string, User>()
 
  find(id: string) {
    // ~~~~ Error: Property 'find' in type 'UserRepo' is not assignable to the same property in base type 'Repository<User>'.
    //      Type '(id: string) => User | null' is not assignable to type '(id: string) => Promise<User | null>'.
    return this.cache.get(id) ?? null
  }
}

Marking the method async wraps the return value and satisfies the declared signature without changing a line of logic:

// ✅ Fixed — async makes the return type Promise<User | null>
class UserRepo extends Repository<User> {
  private cache = new Map<string, User>()
 
  async find(id: string) {
    return this.cache.get(id) ?? null
  }
}

3. React Class state That Does Not Match the Generic Argument

React.Component<P, S> declares state with type S, so your initializer is checked against the second type argument. A string where the generic says boolean — or a typo in a key — surfaces as TS2416 on state, not on the property inside it.

// ❌ Broken
class Filters extends React.Component<FilterProps, { open: boolean }> {
  state = { open: 'yes' }
  // ~~~~~ Error: Property 'state' in type 'Filters' is not assignable to the same property in base type 'Component<FilterProps, { open: boolean; }, any>'.
  //       Type '{ open: string; }' is not assignable to type 'Readonly<{ open: boolean; }>'.
 
  render() {
    return <div>{this.props.label}</div>
  }
}
// ✅ Fixed — the initializer matches the declared state shape
class Filters extends React.Component<FilterProps, { open: boolean }> {
  state = { open: false }
 
  render() {
    return <div>{this.props.label}</div>
  }
}

Annotating the field as state: FilterState (with a named type) is worth doing in bigger components: the error then points at the exact key instead of comparing two inline object types.

4. Arrow-Function Property Narrowing Its Parameter

This one surprises people because the equivalent method compiles fine. Under strictFunctionTypes (part of strict), function-typed properties have their parameters checked contravariantly — the override may accept more than the base, never less.

// ❌ Broken
class BaseHandler {
  onEvent = (event: Event) => {
    console.log(event.type)
  }
}
 
class ClickHandler extends BaseHandler {
  onEvent = (event: MouseEvent) => {
    // ~~~~~~~ Error: Property 'onEvent' in type 'ClickHandler' is not assignable to the same property in base type 'BaseHandler'.
    //         Type '(event: MouseEvent) => void' is not assignable to type '(event: Event) => void'.
    console.log(event.clientX)
  }
}

Keep the declared parameter type and narrow inside the body, where a runtime check makes the assumption honest:

// ✅ Fixed — accept what the base accepts, narrow with instanceof
class ClickHandler extends BaseHandler {
  onEvent = (event: Event) => {
    if (event instanceof MouseEvent) {
      console.log(event.clientX)
    }
  }
}

If the bivariant behaviour is what you actually want, declare the member as a method in both classes — onEvent(event: Event) {} in the base and onEvent(event: MouseEvent) {} in the child compiles. Changing only the child from a property to a method does not work; that is TS2425.

How to Fix It

  1. Read the indented elaboration, not just the headline. The first line only names the member and the two classes. The lines beneath it show the two member types and drill down to the exact incompatible piece — usually a single property or parameter.

  2. Fix the contract in the base, not the symptom in the child. If the subclass legitimately needs a wider type, the base type was too narrow: extend the union, introduce a type parameter (class Repository<T>), or move the member to the subclass entirely. Re-declaring it wider in the child is exactly what TypeScript is rejecting.

  3. Match return types exactly. Promise<T> versus T, void versus boolean, and this versus the concrete class name are the usual culprits. A base method declared to return this must be overridden with a this return type — returning the subclass by name is not accepted.

  4. Keep every overload when you implement an interface. If the base declares serialize(value: string): string and serialize(value: number): string, an implementation with only the string signature is TS2416: your single signature is not assignable to the full overload set. Declare both overloads, or widen the single one to string | number.

  5. Do not reach for as any or @ts-ignore. Silencing the member does not make the substitution safe — callers holding a base-typed reference still pass values your override cannot handle, and the failure moves to runtime. A widened base type or an instanceof check in the body costs the same amount of typing and keeps the guarantee.

  6. Turn on noImplicitOverride and mark overrides with override. It does not prevent TS2416 by itself, but it makes every intentional override explicit, so a signature drifting apart from its base shows up as a deliberate edit in review instead of a surprise error after a dependency upgrade.

FAQ

What causes TypeScript error TS2416?

TS2416 fires when a class declares a member whose type is not assignable to the type of the same-named member in the class it extends or the interface it implements. Common triggers are widening a property's type, returning something other than what the base signature promises, and dropping one of the base overloads.

The check exists because subclassing is a substitutability promise. Wherever the base type is expected, an instance of your class may show up, so every member has to keep working the way the base declared it. A signature mismatch under implements also reports TS2416 at the member; a member that is missing entirely, or private where the interface wants it public, reports TS2420 at the class instead.

Why can a method override narrow its parameter but an arrow property cannot?

Method declarations are checked bivariantly for historical compatibility, so a narrower parameter is accepted. A property holding an arrow function is an ordinary function-typed property, so under strictFunctionTypes its parameters are checked contravariantly and narrowing them is an error.

class BaseHandler {
  onEvent(event: Event) {} // method — bivariant
}
 
class ClickHandler extends BaseHandler {
  onEvent(event: MouseEvent) {} // compiles
}

Neither form is truly type-safe when narrowed — the method version is a known unsoundness the language keeps for compatibility. Prefer the explicit instanceof check in the body if you want the narrowing to be real.

How do I fix TS2416 on state in a React class component?

The state initializer must be assignable to the second type argument of React.Component. Compare the two shapes named in the error: usually a literal is inferred too widely or a key is misspelled, missing, or typed as string where the generic says boolean.

Declaring the state shape as a named interface and annotating the field with it gives much sharper errors than an inline object type, because the compiler then reports the offending key directly. The same applies when a third-party base class is involved: if the mismatch comes from a .d.ts you do not control — a common case with conflicting stream or collection typings — upgrade the type package first; skipLibCheck only hides declaration-file conflicts, it will not silence TS2416 in your own code.

Related Errors

Practice This

Browse all TypeScript practice challenges to keep sharpening your type-level skills.

Related Concepts

Share this reference

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges