Signatures overburdened, types united, and the call error of 'No overload matches'

Consider a TypeScript function that takes either a string or a Promise<string> as input and returns an answer of the same type. Here's an example:

function trim(textOrPromise) {
    if (textOrPromise.then) {
        return textOrPromise.then(value => result.trim());
    }
    return textOrPromise.trim();
}

I want to use generics in defining the function signature to ensure that passing a Promise always results in a Promise, while passing a string gives back a string

To achieve this, I create the following overloads:

function trim(text: Promise<string>): Promise<string>
function trim(text: string): string
function trim(text: any): any {
    if (text.then) {
        return text.then(result => result.trim());  // returns Promise<string>
    }
    return text.trim();                             // returns string
}

This transpiles correctly when the parameter is explicitly defined as string or Promise<string>:

let value: string
trim(value)                          // works fine
let value: Promise<string>
trim(value)                          // works fine

But, when using a union type (Promise<string> | string) for the parameter:

let value: Promise<string> | string
trim(value)                          // error: TS2769

The transpilation error received is:

TS2769: No overload matches this call.
   Overload 1 of 2, '(text: Promise<string>): Promise<string>', gave the following error.
    Argument of type 'string | Promise<string>' is not assignable to parameter of type 'Promise<string>'.
       Type 'string' is not assignable to type 'Promise<string>'.
   Overload 2 of 2, '(text: string): string', gave the following error.
     Argument of type 'string | Promise<string>' is not assignable to parameter of type 'string'.
       Type 'Promise<string>' is not assignable to type 'string'.

Interestingly, adding the union type to the function signature resolves the issue:

function trim(text: Promise<string>): Promise<string>
function trim(text: string): string
function trim(text: Promise<string> | string): Promise<string> | string
function trim(text: any): any {
    if (text.then) {
        return text.then(result => result.trim());
    }
    return text.trim();
}

let value: Promise<string> | string
trim(value)                          // works fine

With this implementation, TypeScript correctly infers that the function returns a Promise or a string based on the input it receives. This contrasts with the behavior suggested by a union of Promise<string> | string in the third overload.

If anyone can explain why this happens and the necessity of overloads for union types, I would greatly appreciate it.

Answer №1

Passing Unions to An Overload

In Typescript, the union cannot be split up before being checked against overload signatures. When a variable of type Promise<string> | string is checked against each overload individually, it is found that the union is not assignable to either of its members, resulting in no overload signature accepting Promise<string> | string.

This behavior has been known for some time with GitHub issues dating back years.

The argument against changing this behavior in Typescript argues that the number of possible combinations can grow exponentially when dealing with a function containing multiple arguments that accept various types.

Because Typescript does not natively support this feature, manually adding an overload that accepts and returns the union is necessary, as you have already done. It's important to note that the implementation signature (the last line of the overload) does not count as one of the overloads, so simply including the union in the implementation signature is insufficient.

function trim(text: Promise<string>): Promise<string>;
function trim(text: string): string;
function trim(text: Promise<string> | string): Promise<string> | string;
function trim(text: Promise<string> | string): Promise<string> | string {
  if (typeof text === "string") {
    return text.trim();
  } else {
    return text.then(result => result.trim());
  }
}

Generics

Using generics instead of overloads avoids the aforementioned issue because extends includes the union. When defined as T extends A | B, it means that T can be A, B, or the union A | B (or any refined version of those).

function trim<T extends Promise<string> | string>(text: T): T {

However, generics introduce their own challenges when implementing the function since refining the type of variable text does not automatically refine the type of generic T. This makes sense considering that T could represent a union.

These complications arise even though we are returning the same type as the input. Despite seemingly limited possibilities (string, Promise<string>, Promise<string> | string), extends allows for infinite potential types for T that extend from these options. Thus, avoiding errors requires using as assertions.

Type 'string' is not assignable to type 'T'. 'string' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | Promise'.(2322)

Additionally, there are unique scenarios where these assertions may be incorrect.

function trim<T extends Promise<string> | string>(text: T): T {
  if (text instanceof Promise) {
    return text.then((result) => result.trim()) as T;
  }
  return (text as string).trim() as T;
}

const a = trim(' ' as Promise<string> | string);  // type: string | Promise<string>
const b = trim(' ');                              // type: ' ' -- actually wrong!
const c = trim(' ' as string);                    // type: string
const d = trim(new Promise<string>(() => ' '));   // type: Promise<string>
const e = trim(new Promise<' '>(() => ' '));      // type: Promise<' '> -- wrong again!

Typescript Playground Link

Despite the extra line required, I prefer using the overloaded version given the complexities involved.

Answer №2

Have you given Conditional types a shot?

If we apply it to your situation, the code may appear like this:

function adjust<T extends string | Promise<string>>(
 content: T
): T extends Promise<string> ? Promise<string> : string {
  ....
}

You also mentioned why necessity of using overload signatures for type checking.

function adjust(content: Promise<string>): Promise<string>
function adjust(content: string): string

This serves as a way to inform TS that when a string is passed, the return type should be string, and if a Promise is passed, the return type should be Promise. Due to TS lacking any runtime type checking capabilities, it cannot determine the type of value during runtime passed to the function.

In addition, no need for a final overloading statement with `any.

function adjust(content: any): any {


function adjust(content: Promise<string> | string): Promise<string> | string {

this will suffice as it covers every scenario defined by the overload signatures above.

Your end result would resemble this:

function adjust(content: Promise<string>): Promise<string>;
function adjust(content: string): string;
function adjust(content: Promise<string> | string): Promise<string> | string;
function adjust(content: Promise<string> | string): Promise<string> | string {
  if (content instanceof Promise) {
   return content.then((response) => response.trim());
  }
  return text.trim();
}

I trust I have adequately addressed your question.

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

How to access class type arguments within a static method in Typescript: A clever solution

An issue has arisen due to the code below "Static members cannot reference class type parameters." This problem originates from the following snippet of code abstract class Resource<T> { /* static methods */ public static list: T[] = []; ...

Error TS2304: The identifier 'Map' cannot be found in version 5.1.0 of Node.js, TypeScript version 1.6.2, and WebStorm 11

While utilizing the filewatchers in my WebStorm 11, I encountered a TS2304 error related to ts-compiler 1.62. The error message reads: TS2304: Cannot find name 'Map' By deactivating the filewatcher and running the command tsc --target es6 app ...

Understanding Multiple Type Scenarios in React with Typescript

Code Demonstration: type PropsType = {top: number} | {bottom: number} // The function that moves something in one direction by a specific distance. function move(props: PropsType) { ... } Expected Usage: move({top: 100}) or move({bottom: 100}) Avoid us ...

Properties of a child class are unable to be set from the constructor of the parent class

In my current Next.js project, I am utilizing the following code snippet and experiencing an issue where only n1 is logged: class A { // A: Model constructor(source){ Object.keys(source) .forEach(key => { if(!this[key]){ ...

The latest release of Angular2, rc1, eliminates all parameters that are not in

In the previous beta version, I was able to analyze using split Location.path(), but now it seems to have been removed. How can I prevent this removal? Interestingly, everything works well with matrix parameters (;id=123;token=asd). This was tested on a ...

Formatting Strings in JavaScript when saving as a .txt file with proper indentation

Utilizing Angular and TypeScript/JavaScript for testing purposes. Each row has been formatted with a newline at the end of the code. formattedStr += car.Name + ' | ' + car.Color + ' | ' + car.Brand + '\r\n' The da ...

What sets 'babel-plugin-module-resolver' apart from 'tsconfig-paths'?

After coming across a SSR demo (React+typescript+Next.js) that utilizes two plugins, I found myself wondering why exactly it needs both of them. In my opinion, these two plugins seem to serve the same purpose. Can anyone provide insight as to why this is? ...

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 ...

Issues with Webpack and TypeScript CommonsChunkPlugin functionality not functioning as expected

Despite following various tutorials on setting up CommonsChunkPlugin, I am unable to get it to work. I have also gone through the existing posts related to this issue without any success. In my project, I have three TypeScript files that require the same ...

Assigning object properties from a request to every item in an observable array of objects using RxJS

Despite my efforts to search various resources like mergeMap and switchMap, I am unable to solve the problem I'm facing as I am still new to RxJs. While I would like to provide more details about my attempts in this post, I fear it may complicate my q ...

Troubleshooting Generic Problems in Fastify with TypeScript

I am currently in the process of creating a REST API using Fastify, and I have encountered a TypeScript error that is causing some trouble: An incompatible type error has occurred while trying to add a handler for the 'generateQrCode' route. The ...

What is the best way to disable a collapsed section in VS Code using comments?

I'm wondering how to properly comment out a "folded" section of code using VS Code. For instance, I want to comment out the collapsible region: if (a == b) { dance(); } I am familiar with the keyboard shortcut for folding regions: Ctrl + Shift + ...

In Typescript 12, the process of creating a Bootstrap popup that waits for the user to click on a value entered in

Greetings! I am currently working with Angular TypeScript 12 and I am attempting to trigger a Bootstrap modal popup when I enter a code in the input field and press enter. However, the issue is that the popup is always displayed even without typing anythin ...

exploring the ins and outs of creating computed properties in TypeScript

How can I store an object with a dynamically assigned property name in an array, but unsure of how to define the array properly? class Driver { public id: string; public name: string; constructor(id , name) { this.id = id; th ...

Angular - Error: Object returned from response does not match the expected type of 'request?: HttpRequest<any>'

While working on implementing an AuthGuard in Angular, I encountered the following Error: Type 'typeof AuthServiceService' is not assignable to type '(request?: HttpRequest) => string | Promise'. Type 'typeof AuthServiceServic ...

Server-side props become inaccessible on the client side due to middleware usage

I'm attempting to implement a redirect on each page based on a specific condition using Next.js middleware. Strange enough, when the matcher in middleware.ts matches a page, all props retrieved from getServerSideProps for that page end up being undef ...

typescript error is not defined

While browsing online, I came across a post discussing how to transfer data from an MVC model to a .ts file. The suggestion was to include the following code: <script type="text/javascript"> var testUrl = @Html.Raw(Json.Encode(Model.testUrl) ...

Angular is able to successfully retrieve the current route when it is defined, but

Here's the code snippet I am working with: import { Router } from '@angular/router'; Following that, in my constructor: constructor(router: Router) { console.log(this.router.url); } Upon loading the page, it initially shows the URL a ...

What is the process for changing the text in a text box when the tab key on the keyboard is pressed in

When a user types a name in this text box, it should be converted to a specific pattern. For example, if the user types Text@1, I want to print $[Text@1] instead of Text@1$[Text@1]. I have tried using the keyboard tab button with e.keyCode===9 and [\t ...

Evolution of ReactJS state over time

When working with React, I wanted to increment a state variable called progressValue by 0.1 every 500 ms until it reaches 100. Here's what I initially tried: const [progressValue, setProgressValue] = React.useState<number>(0) const tick ...