-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Description
Suggestion
When merging union signatures, skip "uncallable" callbacks: those that request a never callback argument. These functions can't be called, so they can be removed from consideration when building the function corresponding to a union signature.
💻 Use Cases
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: TypeScript 4.1
// 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)
// ERR: TypeScript 4.2 (dev)
// Parameter 'item' implicitly has an 'any' type.(7006)
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.
In TypeScript 4.1, the two signatures were considered "incompatible", so attempting to call the function would raise an error. TypeScript 4.2 improves this situation slightly, since the function is now callable; however, it fails to infer the right types for the callback parameters, and so they get defaulted to any.
However, there's a generic rule which can be used to "fix" the union signature so that the correct typing machinery is activated! Since "legal" code can't create values of type never, legal code can't call functions with any argument having type never. For brevity, a callback where any of its required, non-rest parameters have type never is called "uncallable".
To avoid introducing too many new bugs or soundness holes, we can restrict this reasoning just to where union signatures are created. Specifically, when merging two callback types, if one of them is "uncallable" and the other is not, then we use the "callable" one for their intersection.
TypeScript currently handles a more-restrictive form of this check. It's able to see that (for example)
const mapNums: <R>(func: (item: number) => R) => R[]
= null as any;
const mapNever: <S>(func: (item: never) => S) => S[]
= null as any;
// mapSomething inferred: <R>(func: (item: number) => R) => R[]
const mapSomething = Math.random() < 1/2 ? mapNever : mapNums;in this case, mapSomething has the same type as mapNums, since it can verify that the types are "compatible". The problem happens when an "irrelevant" type appears, which often occurs for "methods" since they have an implicit or explicit this parameter:
const mapNums: <R>(func: (item: number, x: string) => R) => R[]
= null as any;
const mapNever: <S>(func: (item: never, y: number) => S) => S[]
= null as any;
// mapSomething inferred: const mapSomething: (<S>(func: (item: never, y: number) => S) => S[]) | (<R>(func: (item: number, x: string) => R) => R[])
const mapSomething = Math.random() < 1/2 ? mapNever : mapNums;now, the x and y parameters prevent tsc from assigning a subtyping relation between the two functions. However, since mapNever still can't be called, its type has effectively not changed from the previous example! So it's safe to merge them by treating it as a subtype still. That's what this change proposes, to handle some of the rough-cases where never doesn't aggressively "poison" the types it's wrapped in.
🔍 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
- specific issue with empty tuples: Possibly empty tuple confuses map and reduce methods #38514
- more elaborate related case for ts(2349) not specific to arrays/
neverUnion of subtypes should be usable as the supertype #38048
I previously opened and closed #42487 since I originally didn't know about the change in behavior in 4.2.
✅ 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.