Why does TypeScript assign parameters in the opposite direction when assigning callbacks?

When a function is called with an argument, the argument type is matched to the parameter type. This means that the argument type must be the same as, or more specific than, the parameter type.

For example:

const foo = (bar: string | number) => {
  console.log(bar);
}

foo('bar');

The code above works perfectly fine because the string literal type 'bar' matches the type string | number.

Now, let's consider a different scenario involving a callback function:

const foo = (bar: (baz: string | number) => void) => {
  bar('baz');
  // We should also try calling the callback function with a number, like bar(42),
  // to match the signature of the callback function.
}

const bar = (baz: string) => {}

foo(bar);

When the code runs, we encounter an error with the line bar('baz'):

Argument of type '(baz: string) => void' is not assignable to parameter of type '(baz: string | number) => void'.
  Types of parameters 'baz' and 'baz' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.ts(2345)

It is interesting to note that in the case of the callback function, TypeScript tries to match (baz: string) => void (argument type) to

(baz: string | number) => void
(parameter type).

However, the parameter of the callback function is matched in the opposite direction: the type string | number (parameter of the callback's argument) is matched to type string (parameter of the callback's parameter).

Why does the direction of type matching in the callback function differ from the non-callback scenario?

Answer №1

In the event that assignments progress in a direction that is opposite, the term "counter-vary" can be used. In fact, function types exhibit contravariance with regards to their input types. This means that a function with the structure (x: X) => void can be assigned to (y: Y) => void if Y can be assigned to X, not the other way around. Specifically,

(baz: string | number) => void
can be assigned to (baz: string) => void, whereas (baz: string) => void is not assignable to (baz: string | number) > void.

To understand this concept better, you can incorporate more functionality in your code that adheres to these types. For instance, within foo(), the parameter bar is meant to accept string | number, allowing you to call bar() with any string or numeric argument of your choice.

 const foo = (bar: (baz: string | number) => void) => {
     bar('baz');
     bar(123); // <-- this is acceptable as well
}

Subsequently, bar accepts strictly a baz argument of type string, indicating that it is acceptable to handle baz like a string, such as invoking its toUpperCase() method:

const bar = (baz: string) => { baz.toUpperCase() } // <-- this is also fine

When trying to execute foo(bar) without correcting the error, you will encounter a runtime error indicating that baz.toUpperCase is not a function.

The ability of a function to handle a string does not automatically imply its ability to handle a string | number.


Conversely, it is completely acceptable to widen the input of a function:

const qux = (baz: string | number | boolean) => {
     if (typeof baz === "string") {
          console.log(baz.toUpperCase())
     } else if (typeof baz === "number") {
          console.log(baz.toFixed())
     } else {
          console.log(baz === true)
     }
}

foo(qux); // okay

This is permitted because any function capable of handling a string | number | boolean can certainly handle a string | number. There are no runtime errors here because within qux(), baz.toUpperCase() is only called after verifying it is a string. (If an attempt is made to call toUpperCase() on a number or boolean, a compiler error will be raised).


Thus, function inputs are assigned in an opposite direction due to counter-variance resulting from the flow of data. The data must not surpass the constraints of the container it is placed in. Therefore, it is always safe to expand the container or reduce the data size, but not vice versa. For function parameters, data is introduced into the function, allowing the function to broaden the input parameter but not make it more restrictive.

Playground link to code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Mocking a common function in a shared service using Typescript and Jest

I have a service that is utilized with NestJS, although the issue at hand is not specific to NestJS. Nonetheless, testing in NestJS is involved, and I use it to create the service for testing purposes. This service is responsible for making multiple calls ...

Update your mappings for the city of Istanbul when utilizing both TypeScript and Babel

Currently, I am facing the challenge of generating code coverage for my TypeScript project using remap Istanbul. The issue arises due to the usage of async/await in my code, which TypeScript cannot transpile into ES5 directly. To circumvent this limitation ...

What is the best way to activate an input field in react-select?

Currently, I am working on a dropdown feature using react-select and have encountered some issues that need to be addressed: 1) The input field should be focused in just one click (currently it requires 2 clicks). 2) When the dropdown is opened and a cha ...

Ways to reload a page in Angular using routerLink and ID

I am working on an Angular application that involves navigation using routerlinks. My goal is to navigate to a specific user page by ID if the page is already open and meets certain conditions. In my .component.ts file, I have the following code snippet: ...

"Encountering a Vue error while attempting to register the Can component globally with CASL +

I have successfully created a vue + typescript application using vue-cli. I followed the instructions from https://stalniy.github.io/casl/v4/en/package/casl-vue and added the following code: // main.ts import Vue from 'vue'; import App from &apo ...

Incorporate Select2 functionality within the Angular2 application

I'm currently working on incorporating the Select2 plugin into my Angular2 application. Successfully, I have managed to set up select2 and transform my multiple select fields as expected. However, I am now facing a challenge in retrieving the selected ...

AsExpression Removes Undefined from Type More Swiftly Than I Prefer

Utilizing an API that returns a base type, I employ the as keyword to convert the type into a union consisting of two sub-types of the original base type. interface base { a: number; } interface sub1 extends base { s1: number; } interface sub2 extends bas ...

Using injected services within static methods may seem tricky at first, but once you

I am exploring the integration of angularjs and typescript in my project. Currently, I am working on creating an Orm factory using typescript but have encountered some challenges. The structure of my factory class is as follows: class OrmModel implements ...

The contents table remains fixed in the top right corner as you scroll

I have developed an Angular app with a table-of-contents component that only displays two items. The code for the script is as follows: ts import { Component, OnInit } from '@angular/core'; import { pdfDefaultOptions } from 'ngx-extended-p ...

What is the best way to perform a deep copy in Angular 4 without relying on JQuery functions?

Within my application, I am working with an array of heroes which are displayed in a list using *ngFor. When a user clicks on a hero in the list, the hero is copied to a new variable and that variable is then bound to an input field using two-way binding. ...

What are the advantages of using interfaces in React?

Exploring Typescript's interface and its application in React has been an interesting journey for me. It seems that interfaces are used to define specific props that can be passed, acting as a form of protection against passing irrelevant props. My qu ...

In Typescript, inheritance of classes with differing constructor signatures is not permitted

While working on implementing a commandBus, I encountered an issue when trying to register command types with command handlers for mapping incoming commands. My approach was to use register(handler : typeof Handler, command : typeof Command), but it result ...

What is the best way to restrict the number of iterations in ngFor within Angular HTML

I want to use ngFor to display a maximum of 4 items, but if the data is less than 4, I need to repeat the loop until there are a total of 4 items. Check out this example <img *ngFor="let item of [1,2,3,4]" src="assets/images/no-image.jpg" styl ...

Tips for refining TypeScript discriminated unions by using discriminators that are only partially known?

Currently in the process of developing a React hook to abstract state for different features sharing common function arguments, while also having specific feature-related arguments that should be required or disallowed based on the enabled features. The ho ...

Integrate a service component into another service component by utilizing module exports

After diving into the nestjs docs and exploring hierarchical injection, I found myself struggling to properly implement it within my project. Currently, I have two crucial modules at play. AuthModule is responsible for importing the UserModule, which conta ...

Attempting to create a function that can accept two out of three different types of arguments

I am trying to create a function that only accepts one of three different types type A = 'a' type B = 'b' type C = 'c' The function should accept either type A, C, or both B and C, but not all three types. This is what I hav ...

Creating a dynamic image binding feature in Angular 8

I am working with an object array that requires me to dynamically add an image icon based on the type of credit card. Typescript file icon: any; savedCreditCard = [ { cardExpiryDateFormat: "05/xx", cardNumberLast: "00xx", cardId: "x ...

The header component does not update properly post-login

I am currently developing a web-app using Angular 8. Within my app, I have a header and login page. My goal is to update the header after a user logs in to display information about the current logged-in user. I attempted to achieve this using a BehaviorS ...

Encountering an error with loading in Angular may require a suitable loader

I am currently working on integrating an AWS QuickSight dashboard into an Angular application. For implementation in Angular, I am referring to the following URL: https://github.com/awslabs/amazon-quicksight-embedding-sdk Could someone provide me with sa ...

Executing Typescript build process in VSCode on Windows 10 using Windows Subsystem for Linux

My configuration for VSCode (workspace settings in my case) is set up to utilize bash as the primary terminal: { "terminal.integrated.shell.windows": "C:\\WINDOWS\\Sysnative\\bash.exe" } This setup allo ...