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

Return type of partially applied bound generic function #47899

Closed
thscott opened this issue Feb 14, 2022 · 3 comments
Closed

Return type of partially applied bound generic function #47899

thscott opened this issue Feb 14, 2022 · 3 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@thscott
Copy link

thscott commented Feb 14, 2022

Bug Report

πŸ”Ž Search Terms

partial application generic bind return type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about bind, generics, partial application

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

function a<T extends Array<any>>(anArray: T): T {
    return anArray;
}

let b = a.bind(undefined, [1]); // b has type () => any[]
let c = () => a([1]);           // c has type () => number[]


// Further simplified

function identity<Type>(arg: Type): Type {
  return arg;
}

let d = identity.bind(undefined, "test"); // d has type () => unknown
let e = () => identity("test");           // e has type () => string

πŸ™ Actual behavior

The types of b and c are different, and the types of d and e are different.

πŸ™‚ Expected behavior

I'd expect them to both have the same type (b that of c, () => number[]), d that of e () => string), as the expressions are doing the same thing (arrow function vs bind for partial application of a function).

This limitation is noted in a comment to the "Strict bind, call, and apply methods on functions" PR, but I couldn't find an actual issue referencing this behaviour.
That comment is also mentioned as a known limitation in a similar issue which was closed in #28920, but unfortunately that pr only fixes the issue when binding only this, and not other parameters.

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

For the case where you exhaust all the arguments, there's really no reason to use bind over a function call.

For the case where you don't exhaust all the arguments, we don't have a type that can represent that - there's not a type annotation on b that you could write that would make it behave the same way as if its bind argument(s) were present at the subsequent call site.

function a<T extends Array<any>>(anArray: T, s: T): T {
    return anArray;
}

// b: ??
let b = a.bind(undefined, [1]); // b has type () => any[]

@matthew-dean
Copy link

matthew-dean commented Jan 19, 2024

@RyanCavanaugh

For the case where you don't exhaust all the arguments, we don't have a type that can represent that - there's not a type annotation on b that you could write that would make it behave the same way as if its bind argument(s) were present at the subsequent call site.

Can you explain why? I'm currently trying to map an object with functions that conform to a specific (albeit generic) interface to function types that retain their generic-ness, and I think it's related to this issue.

In other words, say I have this the following (a super-reduced version of my use-case):

/* example 1 */
type Functions = Record<string, ((data: any, errorMessage?: string) => any)>

const fns = (() => {
  function number(data: string, errorMessage?: string): number
  function number(data: any, errorMessage?: string): { output: 0 }
  function number<T>(data: T, errorMessage?: string): number | { output: 0 } {
      // Do something with errorMessage ...
      if (typeof data === 'string') {
        return Number(data)
      }
      return { output: 0 }
    }

  return { number }
})() satisfies Functions

let foo1 = fns.number({}) // { output: 0 }
let bar1 = fns.number('hi') // number

Now say I want to support this pattern:

/* example 2 */
let doWith = <T, F extends Functions>(value: T, fns: F) => {
  const returnVal = {} as {
    [K in keyof F]: (errorMessage?: Parameters<F[K]>[1]) => ReturnType<F[K]> // ??
  }

  for (const [key, value] of Object.entries(fns)) {
    returnVal[key as keyof F] = value.bind(value, value)
  }
  return returnVal
}

let foo2 = doWith({}, fns).number() // { output: 0 }
let bar2 = doWith('hi', fns).number() // { output: 0 } but should be number

IMO there's enough information here for TypeScript to properly infer types, there's just no syntax for it. In other words, the problem is here:

  const returnVal = {} as {
    [K in keyof F]: Bind<F[K], [T, T]> // Β―\_(ツ)_/Β― - basically some way to pass the type to the function type
  }

TypeScript knows what each function produces in terms of type based on an input type, and that type is known by likewise knowing the input type of the parent function. None of these types are dynamic and they are statically analyzable.

@RyanCavanaugh - re-thinking it, maybe what you are saying is that there isn't currently a type annotation that can do this, in which case this would be a feature request instead of a bug report? In which case, what about a top-level Bind type?

@matthew-dean
Copy link

matthew-dean commented Jan 19, 2024

In other words, a bind statement should be considered by TypeScript to be something like this:

function something<const T>(data: T, message?: string): T {
  console.log(message)
  return data
}
const foo = something.bind(undefined, ['bar'])
// Type of `foo` should be (message?: string) => readonly ['bar']

I don't understand any (technical) reason why that can't be inferred? The information for inference is all there.

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

3 participants