TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Inferring a generic should put the type variable in the inner scope when possible

Open jsoldi opened this issue 3 years ago • 2 comments

Suggestion

🔍 Search Terms

  • generics
  • polymorphism
  • type inference

✅ Viability Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

When inferring a generic type, put the type variable in the inner scope as long as it's possible to make the result as general as possible.

📃 Motivating Example

Say I have a simple composition function:

declare const compose: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => (a: A) => C

The current version of TS allows me to do this and get the correct type:

const comp = compose(<T>(t: T) => t, <T>(t: T) => t); // comp: <T>(a: T) => T

But say I want to wrap the result in an object:

declare const composeObj: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => { value: (a: A) => C }

Now the generic is gone, and comp above is typed as { value: (a: unknown) => unknown }.

I think what's happening is that TS is trying to put the T variable outside the scope of the object because if, instead of a wrapping object, I use a wrapping function, this is what happens:

declare const composeFun: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => () => (a: A) => C
const comp = composeFun(<T>(t: T) => t, <T>(t: T) => t); // comp: <T>() => (a: T) => T

TS is putting T on the outer level, so if I call comp() the result will be (a: unknown) => unknown. A better result would've been to get () => <T>(a: T) => T. This way, calling comp() would not discard the type variable and, if I'm not mistaken, the result would be a more general type than the result we currently get, without breaking any typing rules.

💻 Use Cases

Being able to wrap generic functions when they're the result of type inference. My specific case was writing a type guarding/converting utility using a wrapper around functions of the form a -> Maybe<b> that can be chained together by the dot operator to represent function composition, such that types can be further specified. Something like Thing.isPrimitive.isNumber.isInteger. But this is not possible without the possibility to wrap the result of my compose function.

jsoldi avatar Jun 12 '22 18:06 jsoldi

Related to #30215, which implemented the higher order function inference that makes your first compose work, and describes how such inference only happens in very specific circumstances (which your composeObj does not satisfy because it does not return a function, but an object with a method)

jcalz avatar Jun 13 '22 03:06 jcalz

I've been encountering this a lot recently with some generic code I've written, which takes a generic function and transforms it (a part of it is that it intersects the function with an object). I tried all sorts of hacks to maybe somehow trick the compiler into preserving the genericness, but no dice. Would be really cool if this was improved.

judehunter avatar Mar 09 '25 22:03 judehunter

I'm bumping into this a lot also

declare function wrapWithCustomMethods<Args extends any[], Result>(fn: (...args: Args) => Result): 
    & ((...args: Args) => Result & {customMethod: string})
    // 💣💣 This breaks the inference of the generic
    & {customMethod: string}

declare function id<T>(a: T): T;

// expected: (<T>(a: T) => T & {customMethod: string}) & { readonly customMethod: string; }
// 💣💣 with the extra method it becomes:
// ((a: any) => any) & { readonly customMethod: string; }
const idWithCustomMethods= wrapWithCustomMethods(id);

Any modification in the function signature and the compiler loses it entirelly, there's no workaround for this?

bsunderhus avatar Aug 28 '25 15:08 bsunderhus