You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Let a promise that will resolve to 0;constp=Promise.resolve(0);// This should error but compiler says it is finefunctionp3(): Promise<string>{returnp.then(null);}// I can basically claim that this function returns anythingfunctionp4(): Promise<Window|Date|URL|{anything: "really"}>{returnp.then(null);}// This should also error but compiler says it is finefunctionp5(): Promise<string>{returnp.then(undefined);}
🙂 Expected behavior
// Let a promise that will resolve to 0;constp=Promise.resolve(0);// This should error with: // Type 'number' is not assignable to type 'string'.functionp3(): Promise<string>{returnp.then(null);}// This should error with:// Type 'number' is not assignable to type 'string'.functionp5(): Promise<string>{returnp.then(undefined);}
changed the title [-]Compiler cannot handle with promise.then(null | undefined)[/-][+]Compiler cannot handle promise.then(null | undefined)[/+]on May 22, 2024
This is the correct behavior - at runtime, .then(null) creates a new Promise with the original underlying value propagated out.
vara=Promise.resolve("hello world").then(null).then(x=>console.log(x))// prints 'hello world'
While you might prefer to never allow null | undefined to be passed into .then(), it's both allowed by the spec and permits patterns that are arguably useful (such as optionally-present handlers to transform values).
I don’t think that’s the issue that’s being demonstrated here. Minimal repro:
constpNumber=Promise.resolve(0);constpString: Promise<string>=pNumber.then();// No error
The issue is that without a callback argument to then, the only inference source for its type parameter is the contextual type (in my type annotation here, in the function return types in the repro from blackdyedsheep).
The easy fix I thought of was to add an overload for optional undefined/null, but I’m not sure how well that stacks up against the easiest fix, “just don’t write .then(null).” Is there a real use case for writing code like that?
I think this can be solved with overloads, but because overload resolution doesn’t happen during inference, I think it’s going to be incredibly breaky. #58678 is up to experiment, but I don’t think it will be viable.
Probably nobody calls Promise.resolve(0).then<string>() either except when they do. I'd say it's the same basic issue whether the type is inferred contextually from an unexpected place or the type argument is specified manually: generic defaults don't exactly match the use case, which looks like #56315. The use case in question is something like "when the optional thing is omitted by the caller, it is effectively supplied by the implementer". Generic defaults sort of do this, except when they don't.
I think this can be solved with overloads, but because overload resolution doesn’t happen during inference, I think it’s going to be incredibly breaky. #58678 is up to experiment, but I don’t think it will be viable.
This is fairly accurate and works as expected, for the most part. The specific overloads are all mutually exclusive with each other and are designed to avoid introducing a type variable in the return type of then() that might not be otherwise witnessed when a callback is null, undefined, or not supplied. The catch-all overload allows for various combinations of the specific overloads in cases where you might pass a variable of type (() => T) | null | undefined as it would not match any of the specific overloads despite being perfectly legal.
Unfortunately, it causes #36307 to regress, so it's still not completely reliable.
Ideally, we could solve this during inference by relying on some heuristic related to the fact that TResult1 has a default and no other inferences aside from the return type. Unfortunately, I haven't yet found a reliable heuristic that doesn't break some other expectation somewhere else.
Activity
[-]Compiler cannot handle with promise.then(null | undefined)[/-][+]Compiler cannot handle promise.then(null | undefined)[/+]DanielRosenwasser commentedon May 24, 2024
This is the correct behavior - at runtime,
.then(null)creates a newPromisewith the original underlying value propagated out.While you might prefer to never allow
null | undefinedto be passed into.then(), it's both allowed by the spec and permits patterns that are arguably useful (such as optionally-present handlers to transform values).andrewbranch commentedon May 24, 2024
I don’t think that’s the issue that’s being demonstrated here. Minimal repro:
The issue is that without a callback argument to
then, the only inference source for its type parameter is the contextual type (in my type annotation here, in the function return types in the repro from blackdyedsheep).DanielRosenwasser commentedon May 24, 2024
Whoops, I misread. Apologies.
andrewbranch commentedon May 24, 2024
The easy fix I thought of was to add an overload for optional
undefined/null, but I’m not sure how well that stacks up against the easiest fix, “just don’t write.then(null).” Is there a real use case for writing code like that?blackdyedsheep commentedon May 27, 2024
Yes, there is a real use case, that's how I came across this behaviour.
Quoting Daniel Rosenwasser (@DanielRosenwasser) :
ie this code should infer
Promise<0 | string>but infersPromise<string>and this code should not compile
Basically it should behave the same way as
andrewbranch commentedon May 28, 2024
I think this can be solved with overloads, but because overload resolution doesn’t happen during inference, I think it’s going to be incredibly breaky. #58678 is up to experiment, but I don’t think it will be viable.
jcalz commentedon Jun 9, 2024
Probably nobody calls
Promise.resolve(0).then<string>()either except when they do. I'd say it's the same basic issue whether the type is inferred contextually from an unexpected place or the type argument is specified manually: generic defaults don't exactly match the use case, which looks like #56315. The use case in question is something like "when the optional thing is omitted by the caller, it is effectively supplied by the implementer". Generic defaults sort of do this, except when they don't.rbuckton commentedon Jun 18, 2024
This is fairly accurate and works as expected, for the most part. The specific overloads are all mutually exclusive with each other and are designed to avoid introducing a type variable in the return type of
then()that might not be otherwise witnessed when a callback isnull,undefined, or not supplied. The catch-all overload allows for various combinations of the specific overloads in cases where you might pass a variable of type(() => T) | null | undefinedas it would not match any of the specific overloads despite being perfectly legal.Unfortunately, it causes #36307 to regress, so it's still not completely reliable.
Ideally, we could solve this during inference by relying on some heuristic related to the fact that
TResult1has a default and no other inferences aside from the return type. Unfortunately, I haven't yet found a reliable heuristic that doesn't break some other expectation somewhere else.