-
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
Improve type inference for generic curried functions #15016
Comments
It does not need a high order function to produce the behavior. Type inference fails when generic bound occurs. type GenBound = <T extends string>(r: T) => T
var firstOrder: GenBound = r => r.length |
@HerringtonDarkholme This is a contrived example to demonstrate type inference degradation when generics are involved. The request is to make type inference work for Haskell-style curried functions, I don't actually care about the The actual use case is React higher order components, as illustrated in the second snippet. As the name suggests, they necessarily involve higher-order functions. |
@masaeedu My point here is that the root cause of this problem is not curried function but generic. Note the spec has already said:
So this works as intended. Yet, TypeScript has changed a lot since 1.8. It is not impossible to change the spec. |
@HerringtonDarkholme I understand. This isn't a request for a specific change to type system's behavior. I'm simply trying to illustrate how a common use-case (defining curried functions with out-of-band type signatures) quickly becomes very tedious with the way the type system works right now. |
This earliest report of this issue that I know of is #9366. I've tried to implement a prototype solution using a Hindley-Milner style constraint solver, however I hit on several major problems:
All in all, it's a difficult problem. Unless the team prioritises it and puts considerable effort into figuring it out, I don't think it could be resolved. PS: If interested, you can see my work at https://github.com/gcnew/TypeScript/tree/polyFuncUnification. I tried many approaches, not all of which committed, thus I'm not sure how well the latest commit is working. |
@gcnew I really appreciate the work that must have gone into this investigation. You're probably better equipped to determine whether this is feasible than I am, but I was wondering whether something like F#'s automatic generalization would be possible for function signatures in TypeScript. The idea is simply to infer an implicit generic type parameter for any parameters. So that when I write: function f(foo, bar) {
return foo;
} What actually gets recorded in the type system is: function f<T1, T2>(foo: T1, bar: T2) {
return foo;
} instead of: function f(foo: any, bar: any) {
return foo;
} If this were the case, getting inference to work in the scenario I described above would simply involve the type checker matching up the constraints on type parameters. |
It's actually very hard. You'd like these generics to be reduced to concrete types based on usage and that's far from trivial. You have to factor in overloads, mutability, variadic functions, open-ended objects, unions, subclassing, etc. If you give up on overloads, mutability and unconstrained unions (limit them to only predefined Tagged Unions (ADTs)) it becomes manageable. |
@RyanCavanaugh commented on the issue yesterday, see the discussion on #15114. |
@gcnew The scenario I'm interested in is not really usage-site, i.e. I don't really care about inference of the type args when doing You are right that overloading makes things hairier (as it does in nearly every feature). Could you provide an example of how unconstrained unions/mutability would interact with the proposed change? E.g. if the following: function foo(a, b, c) {
} simply desugared to: function foo<T1, T2, T3>(a: T1, b: T2, c: T3) { } How would unions and mutability affect this? |
I think #3410 is the first issue stating the bug. There are many related issues, but the most important ones are #5616 and #8397. The root cause of the problem is stated here: #3410 (comment). |
I think there is another usecase if it helps : interface A
{
add<T>(name: string, a: T, cb: TypedCallback<T>)
}
interface B
{
add<T, U>(name: string, a: T, cb: Callback<T, U>)
}
type TypedCallback<T> = <U>(a1: T, a2: U) => void;
type Callback<T, U> = (a1: T, a2: U) => void;
var a: A, b: B;
a.add('a', { prop1: 'string' }, function (a1, a2)
{
})
b.add('b', { prop1: 'string' }, function (a1, a2)
{
}) in the case b, everything works as expected, but in case a, a1 is not inferred. |
looks like a duplicate of #5616 |
@mhegazy If you look at @JsonFreeman's comment in #3410, it turns out that these are two different, but somewhat closely related problems. This issue is about improving contextual typing of function expressions by generic function signatures. The issue you linked to is about tightening up function assignability rules. They are not duplicates. |
OK, i think the right one to dupe this against is #15680. though there is overlap with other issues listed above. |
@mhegazy Yup, that seems more similar to this one. |
@mhegazy FYI, looks like this is fixed by #16072. The example above doesn't cause errors with |
@mhegazy, I would open an issue, but after I saw how many other issues of the same subject, even after not finding any issue about this specific case, I'll just ask here... In this second example ( const a = Array("x", "x"); // infer string[]
const b = Array(2).fill("x"); // infer any[], couldn't infer string[] ? The most similar issue that I found was #17428, but I think that it is not the same... |
@lmcarreiro It's not an If you want a general type system where Even if you bite the bullet on the costs of that, it is still a judgement call. What if the person actually intended |
But I don't wanna turn interface ArrayConstructor {
// ...
//there are these two options of constructor that takes only one parameter
(arrayLength?: number): any[]; //option (A)
<T>(arrayLength: number): T[]; //option (B)
// ...
}
// here, it doesn't have enough information to match option (B), it needs to match
// option (A) and use any[] in the assign statement
const a = Array(2);
a.fill("x").fill(1); //both calls here would work because "const a" is any[]
//but here, at the same statement, it does have the information it needs to match to option (B).
//would it be too difficult to infer that T is string and match to option (B)?
const b = Array(2).fill("x");
//and here, the second call to fill method would emit an error
const c = Array(2).fill("x").fill(1); |
@lmcarreiro It doesn't have enough information to match option B. The If you want side effects like There is no way to generalize the logic you're suggesting; if I do |
@masaeedu, you don't understand what I meant...
That is NOT what I want, my suggestion still keeps the statically typed nature of TypeScript
That is what I meant to change, try to use the whole assignment statement to infer the type, NOT the whole function body, example:
Trying to infer
So, the It will assign |
Special casing assignment statements in a language like JS is not a good idea. An assignment statement may contain any number of other things, up to and including entire function expressions, more assignment expressions, narrowing blocks, and picking the "first winner" in such a tree is far from obvious. Whatever logic you propose needs to obey some modicum of equational reasoning. If I do |
To put it another way, expressions should stay abstract wrt their type parameters as long as possible (instead of turning into |
I'd really appreciate better support for good inference in this kind of scenario, whether it is solved by supporting partial generic application or addition of participation of return-types in inference or whatever.
JS libraries that use a functional programming paradigm often end up loosely or any-ly typed, because you find yourself fighting the type system and adding repetitive type noise all over your code to derive any safety. A couple of examples where better support for FP would be useful:
noImplicitAny
project because any generic functions passed or received from the library tend to quickly devolve into{}
unless you repeatedly hand-hold the type systemHere is my original use-case, if interested:
The text was updated successfully, but these errors were encountered: