What is the reason behind Typescript's error message when a function of a union type is called?

Check out the code below (you can see it in action here):

type ParameterlessFunction = () => string;
type ParametrizedFunction = (params: string) => string;

let fn: ParameterlessFunction | ParametrizedFunction;

fn = (p) => `Hello ${p}`;
const calledWithParameter = fn("world");
console.log("calledWithParameter", calledWithParameter)

fn = () => `Goodbye`;
const calledWithoutParameter = fn();
console.log("calledWithoutParameter", calledWithoutParameter)

When assigning values to the variable fn, no errors are thrown as it can be either a parameterless or parametrized function. However, when attempting to call fn(), an error is displayed by the compiler:

Expected 1 argument, but received 0. input.tsx(2, 30): An argument for 'params' was not provided.

What could be causing this issue?

Answer №1

When it comes to assignability, there are some key principles to keep in mind:

If you have two types A and B, where B is a subtype of A, you can safely use a B wherever an A is required.

In addition, take note of the following:

A function A is considered a subtype of function B if A has the same or fewer parameters than B, and:

  1. The 'this' type of A is either unspecified, or a supertype of B's 'this' type.
  2. Each parameter in A is a supertype of its corresponding parameter in B.
  3. The return type of A is a subtype of B's return type.

Where '<:' denotes covariance and '>:' denotes contravariance.

It's worth mentioning that these insights are drawn from the book Programming Typescript by Boris Cherny (O'reilly).

Furthermore, as mentioned by @jcalz in this thread:

When dealing with a union of function types, you can only safely intersect their parameter types. This behavior was intentionally introduced in TS3.2. For instance, if you receive a fnA | fnB, you know you've been handed one of those functions but not sure which. Similarly, with a value of type A | B, you know it's either an A or B, without clarity on which. Thus, calling the former function with the latter parameter may result in ambiguity. However, when dealing with an A & B value, it can be passed to a fnA | fnB function because the parameter fits both types.

Armed with this knowledge, let's delve into deciphering what's going on.

Given that a function can be either parameterless or parametrized, assignments to such variables do not trigger errors.

This holds partly true, as the function can actually be assigned to both ParameterlessFunction and ParametrizedFunction.

Consider the following example:

const foo = (a: (e: string) => string) => {
  return a("foo")
}
foo(() => "bar") // Despite expecting a function that takes a string argument, no error occurs since the passed function indeed returns a string.

Why? As explained earlier, any function that is a subtype of the expected function type can be used safely. Even if the passed function ignores certain arguments, as long as it returns the expected type – in this case, a string – the usage remains valid. Think of Array.map() where not all callbacks utilize the index despite its inclusion in the signature.

Returning to your code snippet:

fn = (p) => `Hello ${p}`; 
const calledWithParameter = fn("world"); 
console.log("calledWithParameter", calledWithParameter)

fn = () => `Goodbye`;
const calledWithoutParameter = fn(); 
console.log("calledWithoutParameter", calledWithoutParameter)

The aforementioned error arises due to Typescript's inability to differentiate between union members like ParameterlessFunction and ParametrizedFunction. In scenarios where functions with varying argument counts are interchangeable, the union type persists. Consequently, the expected argument types resolve to string & empty, leading to the error notification.

To address this issue, consider adjusting the signatures slightly:

type Foo = () => boolean;
type Bar = (params: string) => string;

let fn: Foo | Bar; 

fn = (p: string) => `Hello ${p}`;
const calledWithParameter = fn("Joe"); 
console.log("calledWithParameter", calledWithParameter)

fn = () => true;
const calledWithoutParameter = fn(); 
console.log("calledWithoutParameter", calledWithoutParameter)

By keeping the same parameters while altering the return types, we eliminate errors thanks to Typescript's meticulous assignability checks. In this context, the function aligns solely with Foo due to its boolean return type and zero-parameter expectation.

Hopefully, the clarification provided sheds light on the matter at hand.

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

Using Bootstrap 4 with Angular 2: A Beginner's Guide

Currently, I am in the process of developing an Angular 2 application using TypeScript. My goal is to integrate the Bootstrap 4 framework with some custom theming. Is this achievable? I have encountered issues with the "ng2-bootstrap" npm package, as it d ...

Checkbox functionality included in the Kendo-Ui Angular 2 grid control

My Angular 2 KendoUI GRiD isn't keeping the checkbox checked state when the page changes. I'm using TypeScript for my Angular 2 scripts. ...

Setting attributes for elements in NativeScript/Angular can be accomplished by utilizing item properties

Is it possible to call a function on an item element inside an ngFor loop in order to set some properties? Otherwise, I find myself having to loop twice - once in the script and then again in the template - setting temporary properties to a model that shou ...

Is there a way to modify a single object within an array?

Here is the HTML representation of my data: https://i.sstatic.net/VbKQ4.png page.html <ul id="elements"> <li *ngFor="let elem of fetchdata" (click)="log(elem)"> {{elem.title}} {{elem.description}} </li> ...

Can you explain the significance of the @ symbol in a React import statement?

Currently developing a ruby on rails project using a react js frontend. What does the @ symbol mean in this context? import ProductCard from '@components/search/ProductCard'; ...

Is it possible that using npm link could be the root cause of the "module not

As I delve into understanding how to utilize TypeScript modules in plain JavaScript projects, it appears that I am facing a limitation when it comes to using npm linked modules. Specifically, I can successfully use a module that is npm-linked, such as &apo ...

Tips for transferring information from a Sidemenu to a different page during navigation

Essentially, I have two types of users: Teachers and Students. How can I display different profile screens depending on the user type? The Sidemenu is located in app.components.ts file. ...

Angular function triggered with a double click

In my application, I have created a custom button component that triggers a specific function from the parent component where it is imported. For example, in a login form, clicking the button should call the login() function. The issue I'm facing is ...

What is the best way to incorporate a module from an external 'include' folder in your code?

Within my project's tsconfig.json, I have specified the following: "include": [ "src/**/*", "generated**/*" ] In the directory, there exist two files named src/main.ts and generated/data.json. The task at hand is to be able to successfully ...

Creating a design that verifies every initial letter is capitalized

Looking for an Angular pattern that specifically checks if the first letter of each word is capitalized. To achieve this, I have implemented the following pattern: pattern ="^([A-Z][a-z]*((\\s[A-Za-z])?[a-z]*)*)$" 1- This pattern only works for ...

Maximizing the functionality of rowDoubleClick in Angular for consistent use across various components with shared ag-grid instances

I have implemented an ag-grid in 4 different Angular Components. My goal is to only enable the rowDoubleClicked functionality for one specific component. Unfortunately, when I apply this feature to the grid, it becomes enabled for all components. How can ...

Type of tuple without a specific order

Exploring Typescript typings has led me to ponder how to create a type that is a tuple with unordered element types. For example: type SimpleTuple = [number, string]; const tup1: SimpleTuple = [7, `7`]; // Valid const tup2: SimpleTuple = [`7`, 7]; // &ap ...

When refreshing in Angular, the Local Storage service returns an undefined value for the getItem

In an attempt to store a value in localStorage and retrieve it upon refresh, I have developed a local-storage service to set the value by calling the service. When trying to retrieve the value on refresh, I found that my appComponent's ngOnInit metho ...

Unlocking the Power of Angular 12: Leveraging the Subscribe Method to Access Multiple REST APIs

We have a task where we need to make multiple REST API calls from the ngOnInit() method, one after the other. After making the first call, we need to pass the response to the second API call, and similarly for the third call, we need to get the value from ...

Develop a custom function in Typescript that resolves and returns the values from multiple other functions

Is there a simple solution to my dilemma? I'm attempting to develop a function that gathers the outcomes of multiple functions into an array. TypeScript seems to be raising objections. How can I correctly modify this function? const func = (x:number, ...

typescript-react-router-dom common errors

I am experiencing a typescript error related to Switch and Route from react-router-dom, and I am unsure of the cause. It is possible that there is an incompatibility issue with library versions, but I have been unable to find any information on this. Swit ...

After defining Partial<T>, encountering an error trying to access an undefined property is unexpected

In my function, I am attempting to standardize certain values by specifying the whole function type as Partial. However, despite declaring the interaction variable as Partial Type, I keep encountering the error message saying "Cannot read property endTime ...

Can one generate an enum based on a specific type?

I have a preference: preference Preference = 'OptionA' | 'OptionB' Is it feasible to generate an enumeration from this preference? For instance: enumeration Enum = { OptionA, OptionB } I am uncertain about the feasibility of this. ...

Validating multiple conditions in Typescript by passing them as function parameters

As a beginner in TS/JS, I am looking to validate multiple conditions passed as arguments to a function. For instance, currently I am verifying the user role name, but in the future, I may need to check other conditions. validateUserDetails(): Promise< ...

Prisma queries are not automatically inferring Typescript types

In my Prisma schema, I have defined the following model: model User { id String @id @default(uuid()) name String email String @unique } After writing the TypeScript code below, I expected the return type o ...