TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Generic function constraints degrade in conditionas but work in higher order functions

Open peterhudec opened this issue 10 months ago • 5 comments

🔎 Search Terms

  • generic
  • constraint
  • higher order generic
  • higher order generic function
  • constraint generic

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=4.0.5#code/PTAEAUCcFMGdsgNzqAxgewHawC6QIYCWmOso6AZqBZgLABQOAngA7SgDqBLAStDgFdImAIKgAvKAA8Y6AA8c0TABMyAzAGtM6AO6YA2gF0ANKB4A+ABQ0AXKEsA6J-kgBzWHZEBKCebM-xcwZQeycHF3dPAL8Abx1uDzMAXwYGEFAAEWhXAmUUDGw8IhIySlAAFVAcdFB1LV06RlZ2LnxefiFMACEpcr9JSvlFFTJHZzdE4goEUG9fUCmZnmCQ0AB+UPHI2ejQOIS7HhT6VdA7TGhkSFT6PNQAGxd2AtxQWHQAW2gAMUw7KXAAEZTOAAExWIF2IGmFigqGg3b6aEQUGGG53R4wNBYV7xNp8QTCER2VrtQmiBgYp7YwqgPFkzpdEncAmMqTMNhld5fX5BeiU6APakvHCgOR2ADk+AARqgJQKhViRaAmHZAaCAMw3dIAYRxRWIpHIVG5P0woGpLBg8CQ0GUDGV+AkdJZHSJllNvy8ljkpiYPnS+zaiX0UtlEtM6o1hmOjqdknprPdibdog9nzNXm9vpVAbAQZYiQLIbDcsjmpjxzSYD1hQIhtKJozv1AeRy+DyyiqNTq2j0Dv1oGlzpT5K6Pr9eb29JDvYapjnehjQA

💻 Code

// Preserves constraints of fn
type WrapReturnA = <A extends unknown[], R>(fn: (...args: A) => R) =>
  (...args: A) => {wraps: R}

// Degrades constraints of T to unknown
type WrapReturnB<T> = T extends (...args: infer A) => infer R
    ? (...args: A) => {wraps: R}
    : never

declare const someFn: <P1, P2>(P1: P1, p2: P2) => [P1, P2]

declare const wrapReturnA: WrapReturnA
declare const wrapReturnB: WrapReturnB<typeof someFn>

declare const x: 'abc'
declare const y: 123

// Constraints of someFn are preserved
const a = wrapReturnA(someFn)(x, y) // {wraps: ['abc', 123]}
const aa = wrapReturnA(wrapReturnA(someFn))(x, y) // {wraps: {wraps: ['abc', 123]}}

// Constraints of someFn degraded to unknown
const b = wrapReturnB(x, y) // {wraps: [unknown, unknown]}

🙁 Actual behavior

Wrapping (intercepting/decorating) a function (to for example wrap the return value) with generic constraints by inferring its arguments and return value in a generic type degrades the function's constraints to unknown:

type WrapReturnB<T> = T extends (...args: infer A) => infer R
    ? (...args: A) => {wraps: R}
    : never

The constraints are however preserved and propagated if the type is a generic (higher order) function:

type WrapReturnA = <A extends unknown[], R>(fn: (...args: A) => R) =>
  (...args: A) => {wraps: R}

🙂 Expected behavior

It would be great if the generic constraints of a function wrapped with inference in a conditional type would not be degraded to unknown.

Additional information about the issue

No response

peterhudec avatar Mar 06 '25 09:03 peterhudec

It's a current design limitation. A conditional type like this can't produce a generic function, type parameters are erased and replaced with what got inferred for them (and that can be their constraints, like unknown). It's not about what is produced by a call to wrapReturnB. It's just about what wrapReturnB has been computed as initially:

declare const wrapReturnB: WrapReturnB<typeof someFn>
//            ^? const wrapReturnB: (P1: unknown, p2: unknown) => { wraps: [unknown, unknown]; }

Andarist avatar Mar 06 '25 10:03 Andarist

Thanks for the clarification @Andarist. Is there any other way how to derive a type from a generic function (to tweak its signature) and keep the constraints?

peterhudec avatar Mar 06 '25 10:03 peterhudec

You already have an example of that in your playground. A runtime-level call can produce a new "adapted" generic signature and you are already returning one from this call: wrapReturnA(someFn). The return type of this is <P1, P2>(P1: P1, p2: P2) => { wraps: [P1, P2]; }

Andarist avatar Mar 06 '25 10:03 Andarist

Yeah I know, but I meant purely on the type level

peterhudec avatar Mar 06 '25 10:03 peterhudec

I don't think it's possible today.

Andarist avatar Mar 06 '25 10:03 Andarist

This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar Mar 23 '25 01:03 typescript-bot