TypeScript 6.0 Strict Function Types: Why Contravariance Breaks Your Existing Callbacks This article was written with the assistance of AI, under human supervision and review.
Most TypeScript migration failures stem from a single misunderstood compiler flag: .
The pattern that breaks production is deceptively simple—a callback that accepts a base type where the consumer expects a derived type.
TypeScript 6.0 enables strict mode by default, which means codebases that never configured contravariance checking will fail to compile overnight.
The failure mode here is subtle but expensive.
A callback registered to an array method expects , but the implementation passes .
Pre-6.0 TypeScript allowed this through bivariant parameter checking.
Post-6.0, the compiler rejects it as unsafe.
Teams scramble to fix hundreds of type errors without understanding the underlying variance rules, often choosing or incorrect casts that introduce runtime bugs.
The distinction between function properties and method signatures becomes critical—one enforces contravariance, the other permits bivariance for historical reasons. %% alt: Bivariant checking allows derived types where base types are expected The correct approach requires understanding contravariance: function parameters must accept types that are the same or less specific than what the function signature declares.
When activates, TypeScript enforces this rule for function properties but not method signatures.
The solution is not to weaken types with , but to restructure callbacks using proper variance-aware patterns or switch to method syntax where bivariance is intentional. %% alt: Contravariant checking enforces parameter safety at compile time This matters because the TypeScript 6.0 ecosystem assumes strict mode.
Third-party libraries ship types built for contravariance.
Disabling to silence errors creates a type system that diverges from reality, where the compiler promises safety it cannot enforce.
Production teams need a clear migration path that preserves type safety while fixing legitimate variance violations.
Key Takeaways TypeScript 6.0 enables by default, enforcing contravariant parameter checking for function properties and breaking callbacks that relied on bivariant behavior.
Contravariance requires function parameters to accept the same or more general types than declared—a callback accepting cannot safely be called with under strict checking.
Method signatures retain bivariant checking for compatibility, while function properties enforce contravariance—the choice between and determines variance behavior.
Migration strategies include widening parameter types to unions, using method syntax where bivariance is intentional, or introducing generic constraints that preserve assignability without .
The failure mode is expensive: disabling strict checking or using type assertions creates a false sense of safety while reintroducing the runtime bugs contravariance was designed to prevent.
What Is Contravariance and Why Does TypeScript Care?
Contravariance describes how function parameter types behave when assigning one function to another.
The rule is counterintuitive at first: a function that accepts a more general parameter type can substitute for one that accepts a more specific type.
In other words, if a callback expects , you can safely pass a function that accepts , but not the reverse.
TypeScript enforces this through contravariant parameter checking when is enabled.
The reason this matters is that function consumers control what arguments they pass.
When you register a callback with , the runtime will invoke that callback with instances.
If the callback signature declares , TypeScript must verify that every operation inside the callback body is safe for the broader type.
Accepting a function that expects would allow code like to execute on a generic , causing a runtime crash.
The implication here is that parameter types are checked in the opposite direction from return types.
Return types are covariant—a function returning can substitute for one returning because the caller receives a more specific type than expected, which is always safe.
Parameter types are contravariant—the function must accept everything the caller might pass, which means broader types substitute for narrower ones. %% alt: Contravariant checking ensures parameters are safe for caller's arguments Before TypeScript 2.6, parameter checking was bivariant—the compiler accepted both directions of assignability for convenience.
This allowed patterns like event handlers and array methods to work without type gymnastics, but it also permitted unsafe assignments that could fail at runtime.
The flag introduced contravariant checking as an opt-in safety measure.
TypeScript 6.0 makes it mandatory by enabling strict mode by default.
The challenge for migration is that codebases written without strict checking often contain hundreds of callbacks that violate contravariance.
The compiler suddenly flags these as errors, and teams must decide whether to fix the underlying types or weaken the type system to suppress warnings.
The correct choice preserves contravariance and restructures code to match the safety guarantees TypeScript now enforces.
How strictFunctionTypes Changes Function Parameter Checking The flag alters how TypeScript compares function signatures during assignability checks.
Without the flag, the compiler permits bivariant parameter checking—a function expecting can be assigned to a variable typed as , and vice versa.
With the flag enabled, parameter positions enforce strict contravariance for function properties, rejecting assignments where the parameter type is more specific than the target.
Function properties are those declared with the arrow syntax: .
These enforce contravariance under strict mode.
Method signatures use the method syntax: .
These retain bivariant checking for backward compatibility with classes and object literals.
The distinction is critical because identical-looking code behaves differently based solely on syntax.
The reason for this split behavior is pragmatic.
Classes often override methods with parameters that are more specific than the base class signature, a pattern common in object-oriented hierarchies.
Forbidding this would break vast amounts of existing code that relies on covariant overrides.
The TypeScript team chose to preserve bivariance for method signatures while enforcing contravariance for function properties, which are primarily used in callback contexts where strict checking prevents bugs. %% alt: Syntax determines whether TypeScript enforces contravariance or permits bivariance The migration challenge is that most callback code uses arrow functions and function properties, which means strict mode will flag real issues.
Array methods like , , and all accept function properties.
Event handlers registered through use function properties.
Promise chains with callbacks use function properties.
Each of these patterns must respect contravariance or refactor to method syntax if bivariance is genuinely needed.
The failure mode here is subtle but expensive.
Developers who misunderstand variance rules often respond to strict errors by switching from function properties to method signatures without considering whether bivariance is appropriate.
This silences the compiler but reintroduces the unsafety that strict checking was designed to prevent.
The correct approach is to fix parameter types first and only use method syntax when bivariance is the intentional design.
Real-World Breaking Changes: Arrays, Event Handlers, and Nested Callbacks Array methods are the most common source of contravariance errors when migrating to strict mode.
Consider a callback passed to that declares a parameter of type .
Pre-strict TypeScript allowed this because bivariant checking accepted both directions.
Strict mode rejects it because the array will invoke the callback with instances, and the callback signature promises to
