-
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
Higher order function type inference #30215
Conversation
Is this the correct behaviour? function ap<A>(fn: (x: A) => A, x: A): () => A {
return () => fn(x);
}
declare function id<A>(x: A): A
const w = ap(id, 10); // Argument of type '10' is not assignable to parameter of type 'A'. [2345] I think the logic that floats type parameters out when the return value is a generic function might be too eager. In existing cases where I haven't looked into this much, so apologies in advance if I've got something wrong. |
@jack-williams The core issue here is the left to right processing of the arguments. Previously we'd get it wrong as well (in that we'd infer |
@ahejlsberg Ah yes, that makes sense. I agree that it was previously 'wrong', but I disagree that it was a loss of type safety (in this example). The call is perfectly safe, it's just imprecise. Previously this code was fine: const x = ap(id, 10)();
if (typeof x === "number" && x === 10) {
const ten: 10 = x;
} but it will now raise an error. Could this be a significant issue for existing code? I'll just add the disclaimer that this is really cool stuff and I'm not trying to be pedantic or needlessly pick holes. I'd like to be able to contribute more but I really haven't got my head around the main inference machinery, so all I can really do is try out some examples. |
The safe/imprecise distinction is sort of lossy given certain operations that are considered well-defined in generics but illegal in concrete code, e.g. function ap<A>(fn: (x: A) => A, x: A): (a: A) => boolean {
return y => y === x;
}
declare function id<A>(x: A): A
let w = ap(id, 10)("s"); or by advantage of covariance when you didn't intend to function ap<A>(fn: (x: A) => A, x: A[]): (a: A) => void {
return a => x.push(a);
}
function id<A>(x: A): A { return x; }
const arr: number[] = [10];
ap(id, arr)("s");
// prints a string from a number array
console.log(arr[1]); |
I think being lossy in isolation is ok; it's when you try and synthesise information that you get into trouble. The first example is IMO completely fine. The conjunction of union types, and equality over generics makes comparisons like that possible. function eq<A>(x: A, y: A): boolean {
return x === y;
}
eq<number | string>(10, "s");
eq(10, "s"); // error It's just the behaviour of type inference that rejects the second application because it's probably not what the user intended. I accept that my definition of 'fine' here comes from a very narrow point-of-view, and that being 'fine' and unintentional is generally unhelpful. I agree that the second example is very much borderline, though stuff like that is going to happen with covariance because you end up synthesising information. I'm not trying to defend the existing behaviour; I'm all in favour of being more explicit. My only concern would stem from the fact that the new behaviour is somewhat unforgiving in that it flags the error immediately at the callsite. Users that previously handled the On a slightly different note I wonder if the new inference behaviour could come with some improved error messages. There is a long way to go until TypeScript reaches the cryptic level of Haskell, but with generics always comes confusion. In particular, I wonder in the case of: const w = ap(id, 10); The error message is |
best day ever! thank you so much @ahejlsberg and the team you made my day! i don't really know what to wish for now (except HKT's 😛 ) |
The call to |
I'm creating a declaration file for a function component that accepts generic props. declare interface HeaderMenuItem<E extends object = ReactAnchorAttr> extends React.RefForwardingComponent<HTMLElement, HeaderMenuItemProps<E>> { } How can I export this as a constant and as the default export so <HeaderMenuItem<{ extendedProp: string }> extendedProp="foo" .../> will work? |
export const HeaderMenuItem: <E extends object = ReactAnchorAttr>(props: React.PropsWithChildren<HeaderMenuItemProps<E>>, ref: React.Ref<HTMLElement>): React.ReactElement; |
That works, but was hoping it was possible to do something with the interface so all future changes on that interface are propagated to my definition. Is it not possible? |
Not really, no. An interface simply can't have free type variables in the location you need for a generic component. |
Hello @ahejlsberg ! I'm facing a weird issue regarding higher order function type inferance and I'd like your insight. I'm trying to type a compose function and can't get it to work, Here's what I have declare function compose4<A extends any[], B, C, D, E>(
de: (d: D) => E,
cd: (c: C) => D,
bc: (b: B) => C,
ab: (...args: A) => B,
): (...args: A) => E;
const id = <T>(t: T) => t
const compose4id = compose4(id, id, id, id) The error is positionned on the second parameter
On the other hand, the same function with parameters in piping order works fine : declare function pipe4<A extends any[], B, C, D, E>(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
): (...args: A) => E;
const pipe4id = pipe4(id, id, id, id) Is this by design ? Do you think of any work around I might be able to use here ? Thanks again for everything ! |
My bad found the related issue : #31738 |
Further that, is there a reason why something this wouldnt work? const Test: <PayloadType extends unknown, E = HTMLUListElement>(props: SuggestionProps<PayloadType> & RefAttributes<E>) => ReactElement
= forwardRef((props: SuggestionProps<PayloadType>, ref: Ref<E>) => {
return <h1>test</h1>;
}); cc @weswigham |
@maraisr const Test: <PayloadType extends unknown, E = HTMLUListElement>(
props: PayloadType & React.RefAttributes<E>
) => React.ReactElement = React.forwardRef(
<PayloadType extends unknown, E = HTMLUListElement>(props: PayloadType, ref: React.Ref<E>) => {
return <h1>test</h1>;
}
);
// ERROR: 'unknown' is assignable to the constraint of type 'PayloadType', but 'PayloadType' could be instantiated with a different subtype of constraint 'unknown'. So right now, at least as far as I can tell, the answer is to add It wouldn't be so bad if we could just ignore the specific error above, but sadly ts-ignore will ignore all issues. ref: #19139 |
Hi there! What commit is this merged in? It's difficult to find in the issue since there's a lot of mentions. Thanks |
Thank you @DanielRosenwasser! |
Hello 👋. I'm trying to create a interface ProviderHandler<A extends any[] = any[], R = any> {
(this: RequestContext, ...args: A): R
}
type FlatPromise<T> = Promise<T extends Promise<infer E> ? E : T>
interface WrapCall {
<T extends ProviderHandler>(provider: Provider<T>): T extends ProviderHandler<
infer A,
infer R
>
? (...args: A) => FlatPromise<R>
: never
}
interface RequestContext {
/* ... */
call: WrapCall
}
class Provider<T extends ProviderHandler = ProviderHandler> {
handler: T
constructor(options: { handler: T }) {
this.handler = options.handler
}
}
const context = { /* imagine it implemented */ } as RequestContext Everything works great if handler is a simple function: const p1 = new Provider({
async handler() {
return 5
}
})
// r1 = number
const r1 = await context.call(p1)() But it does not work with generic handlers: const p2 = new Provider({
async handler<T>(t: T) {
return t
}
})
// actual: r2 = unknown
// expected: r2 = string
const r2 = await context.call(p2)('text') |
With this PR we enable inference of generic function type results for generic functions that operate on other generic functions. For example:
Previously,
listBox
would have type(x: any) => { value: any[] }
with an ensuing loss of type safety downstream.boxList
andflipped
would likewise have typeany
in place of type parameters.When an argument expression in a function call is of a generic function type, the type parameters of that function type are propagated onto the result type of the call if:
For example, in the call
as the arguments are processed left-to-right, nothing has been inferred for
A
andB
upon processing thelist
argument. Therefore, the type parameterT
fromlist
is propagated onto the result ofpipe
and inferences are made in terms of that type parameter, inferringT
forA
andT[]
forB
. Thebox
argument is then processed as before (because inferences exist forB
), using the contextual typeT[]
forV
in the instantiation of<V>(x: V) => { value: V }
to produce(x: T[]) => { value: T[] }
. Effectively, type parameter propagation happens only when we would have otherwise inferred the constraints of the called function's type parameters (which typically is the dreaded{}
).The above algorithm is not a complete unification algorithm and it is by no means perfect. In particular, it only delivers the desired outcome when types flow from left to right. However, this has always been the case for type argument inference in TypeScript, and it has the highly desired attribute of working well as code is being typed.
Note that this PR doesn't change our behavior for contextually typing arrow functions. For example, in
we infer
any
(the constraint declared by thepipe
function) as we did before. We'll infer a generic type only if the arrow function explicitly declares a type parameter:When necessary, inferred type parameters are given unique names:
Above, we rename the second
T
toT1
in the last example.Fixes #417.
Fixes #3038.
Fixes #9366.
Fixes #9949.