ProgrammingFrontend/Backend Developer

How is the Strict Function Types option implemented and how does it work in TypeScript? How does it affect the type checking of functions with covariance and contravariance, and in what cases will signature mismatches lead to compilation errors?

Pass interviews with Hintsage AI assistant

Answer.

Background

By default, TypeScript allows some "looseness" in matching function type signatures, permitting that contravariant and covariant functions are considered compatible. Starting from TypeScript 2.6, the strictFunctionTypes option was introduced, ensuring strict type checking for functions and preventing many classes of errors, especially in large codebases.

Problem

Without strict checking, there may be a situation where a handler function or a callback takes more or more specific parameter types, and this goes unnoticed by the developer. This leads to runtime errors associated with the covariance of return types and contravariance of arguments.

Solution

The strictFunctionTypes option introduces strict contravariance for function parameter types. Now functions are compatible only if the source type parameter is a supertype of the target parameter, and not the other way around.

Code example:

type Animal = { name: string }; type Cat = { name: string; meow: () => void }; let animalHandler: (a: Animal) => void; let catHandler: (c: Cat) => void; animalHandler = catHandler; // Error in strictFunctionTypes: argument is too specific catHandler = animalHandler; // Allowed, Cat is a subtype of Animal

Key features:

  • Function arguments are checked for compatibility of "supertypes" (contravariance)
  • Return values are checked for covariance (subtypes are allowed)
  • Signature mismatches lead to compilation errors

Trick Questions.

Could a handler with a more specific parameter type be assigned before the introduction of strictFunctionTypes?

Yes, before enabling strictFunctionTypes, TypeScript allowed assigning more specific functions instead of general ones, leading to runtime problems:

enum E { A, B } const f: (e: E) => void = (e: E.A) => {} // Without strictFunctionTypes: allowed

How does strictFunctionTypes affect callbacks with optional parameters?

If the parameters of a callback function make some parameters optional, strict checking will not allow using a function with fewer required parameters in a position where a function with more parameters is expected. This prevents situations where a callback does not receive the needed data.

Will there be compatibility issues when enabling strictFunctionTypes in old projects?

Yes, there is a risk of new compilation errors appearing, as many functions and handlers could have been assigned to each other with contravariance violations. This is often encountered in callbacks or when using APIs of third-party libraries without strict typing.

Typical Errors and Anti-Patterns

  • Using outdated typing of callbacks without strict parameter checking
  • Attempting to assign a function with a more specific/narrow parameter type to a more general handler
  • Ignoring errors when enabling strictFunctionTypes (commenting out the option instead of fixing types)

Real-life Example

Negative Case

In a large project, event handlers take more specific types (MouseEvent instead of the general Event). This is not detected until the strict option is enabled, leading to errors at runtime with different event sources.

Pros:

  • Faster prototyping

Cons:

  • Runtime bugs when event types do not match
  • Difficult debugging after code extension

Positive Case

The project has used strictFunctionTypes from the beginning. When adding new handlers, all discrepancies between types are automatically detected by the compiler. The code becomes more resilient to typos and is easier to maintain.

Pros:

  • Reliability
  • Safety in function and handler passing
  • Predictable behavior during refactoring

Cons:

  • Requires careful design of signatures
  • In some cases, additional wrappers or overloads need to be written for compatibility