-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Exhaustive checking of overloaded function return types #13225
Comments
I'm questioning whether values with types that represents some union of the expected set of possible return types should be allowed to be returned from the function in the first place: function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
let result: number | string = Math.random() > 0.5 ? 54 : "Hi";
return result;
} What benefit is provided by allowing the return value to have type Maybe the programmer "trusts" that it got a result from somewhere, and that result is expected to behave according to the contract described by the overloaded function signature? function trustedFunction(a: number | string): number | string {
// ...
}
function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
let result = trustedFunction(a);
return result;
} Is it really worth allowing this? |
This basically comes down to this example: function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
let result: number | string = ...;
return result;
} Should this be allowed? Yes. Overloads are divorced from implementation and we only do the barest of checking that the signatures are plausibly correct (most of which seems to backfire anyway). Overall we're not looking to increase complexity of inference / etc here when the signature algorithm today is quite simple and explainable, even if it leaves some soundness to be desired. |
I'm assuming this is a decision that was agreed by the whole team. In that case I feel I can't reason out how they came into this strange conclusion. It's one thing to feel disinterested or dis-motivated about a subject such that it doesn't "feel" like the right thing to do (which I suspect is the case here). It's another thing to scheme up a completely absurd line of reasoning and convince yourself and an entire group of people of it. I'm sorry but I feel like I'm wasting my time here. This doesn't sound very serious to me. |
Correct
I'm sorry you feel this way. We've had a lot of different challenges around how we check overload signatures vs the implementation signature and the current approach appears to be at least a local maximum in terms of allowing correct patterns, disallowing incorrect patterns, and providing sufficient freedom to implement the body in a way that isn't overly burdensome. Intersecting these rules with even more complexity in the checking of the function body isn't an area where we'll see good returns from spending our constrained complexity and performance resources. Concrete data we're looking at here:
|
I've posted a suggestion for function overloading that I think would have solve this issue, and let a programmer write such code
And I hope is simple. |
Here's my initial example again: function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
return 54;
}
const result = func("hi"); // result gets type `string`, however the
// actual return type would always be `number`
// (or more specifically, the literal type `54`). I can't understand why this was rejected? I have mentioned clearly It is possible to check return types even while still allowing return values to have union types: By ensuring the union of the all actual return value types exactly matches the union of all overload return types (For simplicity I'm excluding some cases with I wonder if anyone has actually read what I wrote? The only interpretation I have is that the TS team simply doesn't care. They like the status quo and they don't want to change anything. It seems like the only way to have them consider anything is to have multiple independent people nagging about it for years until they give up, and then what do you know? it may turn out it was worth it! |
Yes
Honestly this hurts. We're real people here and we're making decisions to the best of our ability.
Getting feedback from more than one person is actually valuable, yes. If only one person in the world can be bothered to bring something up, it really is not that important.
😢 |
@rotemdan We are not entitled to order the team what to do. We can give suggestions and sometimes our suggestion gets noticed, especially, as you said, if many people support it. This doesn't mean I don't like yours. I don't like the attitude you are currently exhibiting. Having said that you can do more than a mere suggestion - you can take the onus on you and try to implement it yourself. I do belive that a working prototype can get you a long way down the road both at understanding the complexities involved and also giving you credit in the team's eyes. They can use the prototype and weigh whether it's really beneficial and whether they want to support it for forever. As an example, I believe that the current state of generic functions is quite miserable and actually gets in the way of many everyday tasks. That's why I've decided to create a prototype and hopefully convince the team it's worth it (see #9949). |
I was never trying to tell them what to do. It is simply that the apparent reasoning they sometime display is so twisted I've mostly given up on trying to understand it, so I don't care anymore. I'm not sure even they are aware of all the nonsense they have been constantly trying to sell the community over the years (until they changed their mind). Most people buy it, to some degree. I don't. (a classic case is static inheritance which they aggressively tried to reject for years until they finally fixed it. The amusing part is that that the issue is still labeled as I agree that simply implementing the feature myself, or a simplified version of it, might be a more productive way to deal with the situation. This is actually a path I'm seriously considering nowadays. However, I don't feel forking the compiler would be the right way to go, for me at least. I'll try to explain. I simply don't have time to go and deeply study the type-checker code base. I have looked at it, but it seems like working on it would simply move the problem from one place to another. Even if I get past the learning curve, then say I implement some feature, and it takes me a total of 7 unpayed work days to do so, and they reject it or drag it for months it so that I have to constantly rebase and modify the implementation. I'm not sure that's something I'm willing to take part of. Instead I plan to start small by writing Hopefully, at some point, I hope that |
I'll try to summarize how many TS issues go:
What am I going to learn from this? most likely that this is a complete waste of time. Maybe the best solution would be to instead develop an auxiliary type checker, that would use the TS APIs, and would be a cross between a linter and a real, rigorous type-checker, to post-process over a checked program, or as a plugin to the existing compiler that wouldn't require continuous forking and merging of the TS codebase. It may be the future evolution of |
@rotemdan But I don't think signatures must be decoupled from implementation - coupling the two would have solve your issue too.
Twice. Now it was a third. |
The idea is that the compiler would simply go through the types of the values returned at all code paths, create a union of them, and check that the resulting union is structurally equivalent to the union of the expected return types. It doesn't matter if some code paths are never executed at run-time. It would simply mean that the check isn't completely sound, which is not different than most other constructs of the language. Similarly to a regular check of a return value type. the check would never have false positives, i.e. it is not possible that the check would incorrectly miss cases where a value of a specific type is returned. If some code path returns The check would provide more safety than not having it at all, again, like a lot of the checks already done in the language. |
How this would solve this? function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
if (typeof a === "number")
return "oops"; // this doesn't error
else if (typeof a === "string")
return 54; // nor this
} |
That would be accepted, since the union of return value types cover all expected return types. The check is just a coverage check, in some analogy to the idea "code coverage" from automated testing. Checking for matching of argument and return types was the "next stage" I described. It is a separate component. |
Well - that coverage check would be useful only in trivial functions (and its usefulness can be argued for such functions). And I really thins that the problem is that signatures are decoupled from implementation. That's why you don't have that problem in other languages. IMHO that is. |
Anyway - I really don't think that the decision not to implement this was from not caring. |
I suggested a type-checker feature that seems reasonable, increases type safety and is not very difficult to implement. I have no idea why a group of reasonable people would tag it as The case where a union is returned from an overloaded function is mostly an edge case, my side comment about it was a display of personal reservation and questioning about it, based on common sense. They took that side comment and made it look like it was the whole suggestion. That seemed dishonest and got me somewhat upset. The only reasonable conclusion I could make is that either they didn't carefully read what I wrote, or they simply don't care about it. |
Maybe they concluded that there are more edge cases, and that the edge cases are more common than seemed. Also, as a note - what you posted as multiple features at stages, was understood by me as a single feature, implemented and running in stages. But I surely understand the disappointment. |
Feature suggestions are meant to initiate discussions, not define final specifications. Since I am not the designer of the language and have not written the specification or the compiler, it is clear that I am not in a position to work out all the small details and edge cases. Since they did not provide any meaningful feedback, and don't seem to display interest at participating at fruitful two-way discussions, it is not possible for me to improve or amend any proposal that is made. Their closed-door way of handling things and controlling information creates an unfair advantage for them. It is very difficult to an outside individual to provide a reasonable proposal without any interactive feedback. This is not only based on the way they responded here, but also on tens of previous suggestions I made (some of them were not under my real name) over a period of several years. Some of those tanked proposals have inspired new features that were later introduced into the language. |
We'll have to agree to disagree. They did provide feedback, and I don't think they have to provide detailed reports of their meetings, nor make the "meetings" span into boards where people are answering at different times and days. I understand the disappointment, especially after it seemed like the feature is going to make it into the language. |
What you are implying is that this issue tracker is not meant for proposals that are less than trivial, because non-trivial proposals almost always need some sort of feedback loop to evolve to a reasonable form, and the TS team is not willing to provide it. I agree that is most likely the condition, due to this I am not going to spend any more time on this particular issue, and not likely to spend time on similar ones. On a side note, in Typescript many features don't even start as proposals (at least not publicly available ones). They are immediately implemented and posted as pull requests. For comparison the Python PEP (Python Enhancement Proposal) is a multi-way, interactive process, with a well-defined protocol on which everyone is bound, including the original creator of the language (I don't really know if the protocol is operating exactly as described in practice, I'm just giving it as an example of a different way that things could be formulated, not necessarily because I think it is somehow the best). |
This is not what I'm implying. You could post a fixed version of it, which would handle the problems pointed by the team (AKA: feedback). |
If you mean this: function func(a: number): number;
function func(a: string): string;
function func(a: number | string): number | string {
let result: number | string = ...;
return result;
} That was an example @RyanCavanaugh copied and pasted from my side remark about my personal reservations on the idea of returning a union, which is not related to the original proposal. The original proposal handles that case, including in the second stage (simply check if the expected return type is included in the returned union). The third stage doesn't really, but I mostly gave it as a conceptual idea of how the previous two could be extended further. |
Simply checking the returning union would serve nothing on this case. And I once again wonder if your suggestion is single feature, with multiple stages (some of them undecidable at all), or multiple features, making the first stage a feature of its own? |
Currently:
And:
There are several stages for gradually addressing this:
string
or anumber
in this example) or a type that's a assignable to the union of all possible return types (returning a value with typestring | number
might work as well, although it wouldn't be really 'safe') such that the union of all actual return types at all return paths exactly matches the union of all expected return types (I might give an example to clarify what this means later).a
is narrowed tonumber
always return anumber
and when narrowed tostring
, always return astring
(note that this check would only be reliable if the parametera
is not itself reassigned with a value having an incompatible type before the runtime assertion. Unfortunately there is no current way to modify function parameters with the equivalent ofconst
orreadonly
so it has to be determined based on flow analysis).func
is itself a union (I'm not sure if that's currently possible, but it could be useful, see more details about how this can be achieved in the linked comment).The text was updated successfully, but these errors were encountered: