Skip to content

When resolving incompatible signatures, skip callbacks with never arguments [ts(2349)] #42487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
Nathan-Fenner opened this issue Jan 26, 2021 · 1 comment
Closed
5 tasks done

Comments

@Nathan-Fenner
Copy link
Contributor

Suggestion

TypeScript sometimes infers empty arrays as having type never[]. Usually, this doesn't cause problems, until it gets unioned with another type, and you try to call it as a function/method:

function orDefault<T, D>(x: T | null, d: D): T | D {
    if (x === null) {
        return d;
    }
    return x;
}

const xs: string[] | null = ["a", "bc", "def"];

// y: string[] | never[]
const y = orDefault(xs, []);

// ERR: This expression is not callable.
//  Each member of the union type 
//    (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[])
//  | (<U>(callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])
// has signatures, but none of those signatures are compatible with each other. (2349)
const yThen = y.map(item => item.length);
const yChain = orDefault(xs, []).map(item => item.length);

In this example, since the [] is typed as never[], the .map method has a union type with two incompatible types; one possibility is a string, number, string[] argument list returning a generic U, and the other is a never, number, never[] argument list returning a generic U.

Right now, tsc doesn't distinguish between the two; since they're incompatible, it raises ts(2349). This can make it difficult to write nicely-chainable union values, since the inferred values for "empty" or "impossible" variants prevent type inference from working.

However, there's a very easy reason to see why the first overload should be preferred: values of type never shouldn't exist, so a function asking for a never will never be called.

While applying this rule in general is too extreme (since it would likely open up many soundness holes), we can add a small exception to ts(2349): before rejecting the call for having incompatible callback types, check to see if any argument is a callback expecting never as any argument. If so, replace that callback with the most-general function type, (...args: unknown[]) => never. Then attempt to type-check with this type.

We can see that the existing type machinery can handle the code after this "fix":

const func: (<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[])
    | ((...args: unknown[]) => never)
 = null as any;

// item: string
// res: number[]
const res = func(item => item.length);

This change is very narrow, since it only applies to code that:

  • is currently rejected by ts(2349)
  • AND one of the union signature's parameters is a callback
  • AND that callback has an argument with type never

In particular, since it only affects code that today fails to compile, it can't introduce new breaking changes.

Despite being fairly narrow, it should help out inference for many common functions, especially related to empty arrays and other empty collections, including .map, .filter, .flatMap, .reduce, etc.

🔍 Search Terms

  • empty array map
  • "This expression is not callable"
  • Each member of the union type '((callbackfn: (value: Item, index: number, array: Item[]) => U, thisArg?: any) => U[]) | ((callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[])' has signatures, but none of those signatures are compatible with each other
  • ts(2349)
  • never type
  • incompatible callbacks

Vaguely related, but broader/different from this proposal: #40157

List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

📃 Motivating Example

function orDefault<T, D>(x: T | null, d: D): T | D {
    if (x === null) {
        return d;
    }
    return x;
}

const xs: string[] | null = ["a", "bc", "def"];
// does not type-check today, due to ts(2349)
const fixed: number[] = orDefault(xs, []).map(item => item.length);

💻 Use Cases

Making "chaining" libraries more ergonomic, especially when dealing with results.

Improves types for various array methods.

Also applies to other collections, like new Set([]) having type Set<never>; this makes .forEach callable on a union value with new Set([]) as one possible alternative.

@Nathan-Fenner
Copy link
Contributor Author

TypeScript 4.2 ended up changing this behavior in a way I didn't expect. The code is still rejected, but the error message and reasoning is rather different. As a result, I'm going to close this, and reopen with details updated for 4.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant