microsoft/TypeScript

Generic lambda identity function can cheat typesystem easily

mpodlasin opened this issue · 6 comments

Hi. Before I start I want to emphasize that I googled a lot for that problem and found nothing. That being said, my problem seems to be so simple, that I am sure it was discussed before. If that is the case forgive me for spam. I would be just grateful for some link to the explanation of why TS works here the way it works.

I use TypeScript 1.8.10

So... this does not compile (which is good):

function id <A> (a : A) : A {
  return "string";
}

This however DOES (which for me seems super bad):

const id :<A>(a : A) => A = a => "string";

var x : number = id<number>(5); // compiler does not complain at all :(

(I'm using version 1.9.0-dev.20160429)

Hi, I tried to rewrite the example a bit differently:

This errors:

let func: <T>(a: T): T => "abcd"; // error: string is not assignable to T

However when the signature is declared separately, as a type. This invalid return type can be 'hidden' under the declared type, as the call signature type <T>(a: T) => string is allowed to be assigned to the call signature type <T>(a: T) => T:

type Func = <T>(a : T) => T;

let func: Func = <T>(a: T): string => "abcd"; // no error

And then a caller can use any type for T and the compiler would still consider the return type to be T and not string:

let x = func(1234); // 'x' gets type 'number' but the actual value "abcd".

It looks as if the compiler considers these function types compatible, in particular the return type string is seen to be assignable to T.

I'll try to isolate and rewrite it as clearly as possible:

type Func1 = <T>(a: T) => T;
type Func2 = <T>(a: T) => string;

let func1: Func1;
let func2: Func2;

func1 = func2; // no error

And compare it to:

type Func1 = <T>(a: T) => number;
type Func2 = <T>(a: T) => string;

let func1: Func1;
let func2: Func2;

func1 = func2; // error: type 'string' is not assignable to 'number'

I'm aware that function parameters are bivariant, and based on my tests function return types should be covariant. I'm not sure if that has anything to do with this though. There isn't much further information I can provide at this point.

Further investigation:

#3410 includes some information (although almost a year old), explaining this check (or a related one, I'm not sure) is avoided to due performance reasons.
#5616 is very similar to this (though the example there shows the return type assignment in the other direction), and seems to be 'In Discussion'.

Seems like almost any function can be assigned from or to a generic signature. Parameters and the return type are considered to be of type any, so they will even accept void:

declare let func1: <T, V>(a: T, b: V, c: V) => T;
declare let func2: (a: number, b: string, c: void) => void;

func1 = func2; // no error

I'm not sure, if this should be looked in this issue, but currently TypeScript also doesn't look on generic type constraints when it check functions types:

let f1: (input:number)=>{field:number};
let f2: (input:number)=>{field:string};

f1 = f2; // Type is not assignable error
f2 = f1; // Type is not assignable error

let gf1: <T extends {field:number}>(input:number)=>T;
let gf2: <T extends {field:string}>(input:number)=>T;

gf1 = gf2; // No error here, but should be.
gf2 = gf1; // No error here, but should be.

Unfortunately signatures are seen in their "erased" form (with type parameters replaced with their constraints) during assignability checks. This is something we're aware of as a hole but don't yet have a solution that gives satisfactory performance,

@RyanCavanaugh if performance is a problem (presumably due to recursive deep dives) why not limit checks to a certain depth that can be configured from tsconfig.json, who don't care about typesafety sets "depth: 0" and production ready builds "depth: 100"?

tracked by #5616