Skip to content
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

Function generic type inference differs based on parameter ordering #48777

Closed
TomerAberbach opened this issue Apr 20, 2022 · 4 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@TomerAberbach
Copy link

Bug Report

I've come across a case where the ordering of generic function's parameters changes how it infers the return type.

🔎 Search Terms

function generic type inference parameter order

🕗 Version & Regression Information

This is the behavior in every version I tried except for 3.3.3 where instead of unknown[], {}[] is inferred (which is even more wrong so at least we have some progress 😄).

⏯ Playground Link

Playground link with relevant code

💻 Code

type Collector<Collection, Value> = {}

declare const pipe: {
  <Value>(value: Value): Value
  <A, B>(value: A, fn: (a: A) => B): B
}

declare const toArray: <Value>() => Collector<Value[], Value>

declare const collect: {
  <Collection, Value>(collector: Collector<Collection, Value>): (
    iterable: Iterable<Value>,
  ) => Collection
  <Collection, Value>(
    collector: Collector<Collection, Value>,
    iterable: Iterable<Value>,
  ): Collection
}

const inline = collect(toArray(), [1, 2, 3])
    // ^? unknown[]
const piped = pipe([1, 2, 3], collect(toArray()))
    // ^? number[]

declare const alternateCollect: {
  // Flipped
  <Collection, Value>(iterable: Iterable<Value>): (
    collector: Collector<Collection, Value>
  ) => Collection
  <Collection, Value>(
    // Flipped
    iterable: Iterable<Value>,
    collector: Collector<Collection, Value>,
  ): Collection
}

const inline2 = alternateCollect([1, 2, 3], toArray())
    // ^? number[]
const piped2 = pipe(toArray(), alternateCollect([1, 2, 3]))
    // ^? unknown[]

🙁 Actual behavior

inline and piped2 are inferred as unknown[].

🙂 Expected behavior

inline and piped2 are inferred as number[] (like inline2 and piped).

You can see that alternateCollect just reverses the ordering of the parameters of the curried collect function. Ideally the ordering of the function parameters wouldn't affect the type inference. I would expect TypeScript to figure out the more specific type either way.

Also, the fact that this function is curried and that no matter which parameter ordering I chose one of the inferred return types (depending on the calling style) is wrong makes this an unfortunate situation.

I've also tried sprinkling NoInfer around to get Value inferred to a narrower type before Collection, but I couldn't get anything to work.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Apr 20, 2022
@RyanCavanaugh
Copy link
Member

Parameter ordering does matter since we have a fixed number of inference passes. See also #30134

@TomerAberbach
Copy link
Author

Gotcha, is there any workaround or hint I can give TypeScript here to make it infer the more specific thing? Or is it not possible at the moment?

@RyanCavanaugh
Copy link
Member

This appears to do what you want for those testcases:

declare const collect: {
  <Iter extends Iterable<unknown>, Value>(
    collector: Collector<Iter extends Iterable<infer I> ? I : never, Value>,
    iterable: Iter,
  ): Iter;
}

@TomerAberbach
Copy link
Author

Thanks, but I don't think that declaration is actually using Collector.

For example, if I have:

declare const toSet: <Value>() => Collector<Set<Value>, Value>

const inline = collect(toSet(), [1, 2, 3])

inline is still inferred as number[] (instead of Set<number>) because it's always passing along whatever iterable is.

I wasn't super clear about the intended behavior so no worries though!

Also, now that I'm looking at this problem again I wonder if another way to solve this would involve higher kinded types (#1213). Sketch of the idea using made up syntax:

type Collector<Container<*>> = {}

declare const toArray: () => Collector<Array>
declare const toSet: () => Collector<Set>
// etc.

declare const collect: {
  <Value, Container<*>>(
    collector: Collector<Container<*>>,
    iterable: Iterable<Value>,
  ): Container<Value>
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants