TypeScript: Unexpected behavior of generic constraints

The following code snippet compiles without type errors though the type variable T is constrained to string type.


function foo1(f: (s: string) => string)
{
    return f("hello world");
}

function foo2(f: (s: number) => number)
{
    return f(123);
}

function genericBar<T extends string>(arg: T): T
{
    return arg;
}

var x1 = foo1(genericBar);
var x2 = foo2(genericBar);

Non generic version nonGenericBar works as expected.


function nonGenericBar(arg: string)
{
    return arg;
}

var y1 = foo1(nonGenericBar);
var y2 = foo2(nonGenericBar);  // Type error

Of course, genericBar function is useless because constraining a type variable to a primitive type can be replaced by a non generic function like nonGenericBar. However, the behaviour is somewhat unexpected and inconsistent.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 15 (12 by maintainers)

Most upvoted comments

@masaeedu As I recall, there are two separate, but related issues here. One is the refusal to use a generic signature to contextually type a function expression. That is what you’ve changed. The second issue is that when two signatures are compared for assignability, their type parameters are erased to any, which happens after contextual typing. It is this second behavior that’s motivated by fear of slowness, or infinite recursion. So changing the contextual typing rules does not seem likely to impact these concerns.

In terms of semantic consequences, two things come to mind. One is that in general, the effects of contextual typing might not be as local as you might think. If you are passing a function expression as an argument to an overloaded function, the way that argument is contextually typed could affect which overload is selected, if it changes the argument’s compatibility with particular overloads. This could be something to investigate.

The second question has to do with the function instantiateTypeWithSingleGenericCallSignature in checker.ts. The intent is to flow types by instantiating generic functions in certain situations. Here’s an example:

declare function foo<T>(x: T): T;
declare function applyFn<T, U>(arg: T, fn: (x: T) => U): U;
applyFn(0, foo); // Returns number

The contextual signature supplied by fn is not generic in this example. With your change, I’d expect the types to flow even if fn is generic, if the example were something like:

declare function applyFn<T, U>(arg: T, fn: <V extends T>(x: V) => U): U;