-
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
Possible bug when typing a function that receives a function and its parameters #16871
Comments
Reviewing this issue with @leoasis I reduced this example down to this interface Payload {
a: string,
b: number
}
let doFoo: (payload: Payload) => void;
let doBar: (payload: {}) => void;
doBar = doFoo; // I think this should throw an error but it doesn't
let doZar: (payload: { qwe: number }) => void;
doFoo = doZar; // This throws an error as expected |
It seems to behave correctly. Remember the rules for function type equivalency.
interface Payload {
a: string,
b: number
}
let doFoo: (payload: Payload) => void;
let doBar: (payload: {}) => void;
let a:Payload
let b:{}
a = b // error
b = a // OK
doBar = doFoo; // I think this should throw an error but it doesn't
let doZar: (payload: { qwe: number }) => void;
doFoo = doZar; // This throws an error as expected
` |
@sylvanaar I don't think you applied the same rule, since that example you quoted has the same types in the first argument, and here we have a mismatch in types in the same argument. Also, it's not behaving correctly, check this invalid program that type checks successfully: interface Payload {
a: string,
b: number
}
let doFoo = function (payload: Payload): void {
console.log(payload.a + payload.b.toString());
};
let doBar: (payload: {}) => void;
doBar = doFoo
doBar({}); // will throw a runtime error
doBar({ whatever: 123 }) // will throw a runtime error |
Look at the test I did to show you the equivalence of {} and Payload. You can store a Payload in a {} but not the other way around. That is the result you saw. |
I still don't understand how that relates to the examples I showed. Maybe I need more knowledge of the rules that you're applying to associate them, sorry about that :( Anyway, don't you agree that the last example, even if it is "behaving according to the rules", is not what you want from a type checker? Why would it be ok from a type checker perspective to allow me to call a function with a type that is more general than the one that is specified in its signature? That will never be safe to assume, can't think of a scenario where that is a good assumption. Also, I found this fragment of the documentation, in https://www.typescriptlang.org/docs/handbook/type-compatibility.html#function-parameter-bivariance: When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns. Then follows an example that doesn't explain why the "vice versa" has to be that way. And also, in my own experience, this sort of error is not rare, in fact it is very common. Would love to understand a bit more of the rationale of this, and if there's a way to make this be safe. At the very least, would love to understand this and improve the documentation and examples, since right now it's not clear why this decision was made. |
You should really read the FAQ I do agree that bivariance is a real flaw. but I guess they haven't been getting many complaints about it until recently. |
The reason parameters in function types are related bi-variantly is that otherwise common types such as The alternative would be to have read-only, write-only, and read-write forms of every type, along with a strict requirement that every use of a particular type be accordingly annotated. That would make type relationships significantly more rigid and inflexible (in the name of soundness) and would obviously be a huge breaking change for all existing code (e.g. every existing .d.ts file would have to be revised to include proper read-only, write-only, or read-write annotations for every property and parameter in every type). Or, for backwards compatibility, we'd possibly need a fourth "unknown" state for read/write-ability along with checking rules. It's not that this couldn't be done, but we have to weigh the benefits against the additional complexity it would introduce and it's not clear that it would worthwhile, particularly now that we already handle callback parameters co-variantly (see #15104). |
Thanks for taking the time to answer @ahejlsberg! However I'm still not fully understanding the problem, sorry about that. I don't want to take too much time from you with this, so feel free to refer me to documentation or somewhere else if I'm talking nonsense or trivial stuff. Maybe I need more examples to fully understand what you say, since I still find that most of the time you want in parameters to be contra-variant and out parameters to be co-variant. In JS the most common way to get out parameters are callbacks, and that's something you added in 2.4.1, so that solves most of the use cases without adding extra syntax. However, I fail to see any common pattern that would break if we defaulted to contra variant in parameters, if we resorted to other techniques (already available in Typescript) to solve them. The example shown in https://www.typescriptlang.org/docs/handbook/type-compatibility.html#function-parameter-bivariance (the Anyway, not sure how that matches the goals of this project, but would love to see more examples where this breaks stuff and another technique (such as overloading) wouldn't suffice, because if they would, we could have those use cases supported and be safer at the same time. Again, sorry if I'm talking trivial stuff, and feel free to redirect me to other resources where this has already been discussed. |
Imagine the following scenario: function getDivElements(): HTMLDivElement[] {
// Return a list of HTMLDivElements
}
function processElements(e: HTMLElement[]) {
// Do something with list of HTMLElements
}
processElements(getDivElements()); The call to Technically, arrays are invariant and in a perfect world the |
Thank you so much for the detailed and thorough response. I've learned a lot and this made me research more about the problem. I think I understand now the problem with parametrized read-write structures that would become invariant if the in parameters of functions were forced to be contra-variant. However, I still think there's a case to be made about this decision, which may lead to an opportunity to be revisited, as the goal of being safe for reading, holds as long as you don't try to do things like the following (which is what I stumbled upon in the OT): function getDivElements(): HTMLDivElement[] {
return [];
}
function processElements(e: HTMLElement[]) {
// Do something with list of HTMLElements
}
processElements(getDivElements());
// Added a function that returns an array of more general items than HTMLElement
function getElements(): Element[] {
return [];
}
// This correctly type checks, but will cause a runtime error if the original `processElements`
// tries to read any value from the items that is on HTMLElement that is NOT in Element.
const processFoo : (e: Element[]) => void = processElements;
processFoo(getElements()); Also, I know you are aware of this already, but Flow seems to have that restriction and doesn't seem to be much of a problem, it's actually a good thing. In these cases that problem can be solved by slightly modifying the signature of the function like this (or using ReadOnlyArray as you suggest): function processElements<T extends HTMLElement>(e: Array<T>) {
// Do something with list of HTMLElements
} and that would type check fine even with the contra-variant restriction. Also, from my own experience when researching which type checker should I use, this tradeoff comes out a lot in the wild when comparing Flow vs Typescript, and it is mentioned as a disadvantage of Typescript, not as a benefit, so maybe there can be a way to poll the current Typescript user base to see if this would be desired to be added. Maybe something it could be implemented similarly as how Thanks a lot for the time you took to comment and explain the rationale, even if this doesn't end up being considered to be added into Typescript, hope it would serve as explanation of the rationale to improve the docs. I truly think what you just wrote in this issue is way clearer that what is currently in the docs, so I'm willing to contribute some changes to that section with what I learned from you. |
@leoasis You're welcome. Definitely feel free to contribute a change to the docs. Meanwhile, we'll continue to think about possible non-breaking ways to solve this issue. |
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed. |
TypeScript Version: 2.3.4
Code
Expected behavior:
The lines commented with "wrong!" should show errors.
Actual behavior:
Those lines don't show errors
I added the direct calls to
doFoo
at the bottom for clarification of what I'm expecting. If I'm doing something wrong please let me know.The text was updated successfully, but these errors were encountered: