Skip to content

Generic type parameter inferred as {} #8922

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

Closed
wereHamster opened this issue Jun 1, 2016 · 6 comments
Closed

Generic type parameter inferred as {} #8922

wereHamster opened this issue Jun 1, 2016 · 6 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@wereHamster
Copy link

wereHamster commented Jun 1, 2016

TypeScript Version: 1.8.10, also tested with 1.9.0-dev.20160601-1.0 which behaves the same.

The following code should compile but produces an error. It is because the type of x is inferred as Either<{},number>. IOW, the type parameter E is made rigid when it should remain generic.

Code

interface Either<E,V> { e: E; v: V }

function pure<E,V>(v: V): Either<E,V> { return { e: undefined, v }; }
function fail<E,V>(e: E): I<E,V> { return { e, v: undefined }; }

function f(x: Either<string,number>) {}

const x = pure(1);
f(x); // <- error

A similar issue comes up when inferring the type of this constant:

const x = true ? pure(42) : fail("string");

TypeScript infers Either<{},number> | Either<string,{}> instead of the expected Either<string,number>.

To fix the first problem I could define pure to return Either<any,V>. But I don't want to do that, to allow TypeScript to automatically infer E from context. Such as in this case:

// The Monad `bind` function
function bind<E,A,B>(x: Either<E,A>, f: (a: A) => Either<E,B>): Either<E,B> {
    // Incomplete implementation of this function! Only for illustration purposes.
    throw new Error('Implement me!');
}

const x = bind(pure(42), n => fail("error"));

TypeScript infers x as Either<{},{}>, but it should be Either<string,V>. The E type parameter must be the same in both arguments as well as the return type of the whole function, and because the second argument (the function f) binds it to a string, it should remain a string.

If I change pure to return Either<any,V> and fail to return Either<E,any> then the type of x changes to Either<any,any>, which is not much better.

It would be nice to have an option like noImplicitAny to make it an error if TypeScript infers {} as a type parameter.

I could solve all these issues by always explicitly attaching the types to function calls and var/let/const statements. But that adds a lot of noise to the code, which is particularly annoying for simple cases where TypeScript should be able to infer the correct types.

@sandersn
Copy link
Member

sandersn commented Jun 1, 2016

It sounds like you want two main things: (1) variables to have types with unbound type variables. The shortest example of your problem is

interface Either<E,V> { e: E, v: V }
declare function pure<E,V>(v: V): Either<E, V>
const x: Either<E,number> = pure(1); // what is E? TypeScript needs to know.

TypeScript just can't do this -- it never carries around type variables with a value's variable. And it only infers from argument types.

It also looks like you want whole-program type inference, in order to infer the type from two different branches of a conditional:

const x: Either<string, number> = true? pure(42) : fail("string");

TypeScript definitely does not do this -- it always works bottom up except when contextually typing, which only applies to a few constructs that would otherwise nearly always need type annotations, like object literals and lambdas.

@DanielRosenwasser worked on an error for failure when type inference produces {}, so he can comment on the feasibility of that.

The short summary is that TypeScript's type system is so far from Haskell that it's not feasible to provide many of Haskell's features.

@sandersn sandersn added Design Limitation Constraints of the existing architecture prevent this from being fixed Too Complex An issue which adding support for may be too complex for the value it adds labels Jun 1, 2016
@ahejlsberg
Copy link
Member

With the nightly build and strict null checking mode you can get pretty close by using undefined as the unbound type:

// Compile with --strictNullChecks

interface Either<E, V> { e: E | undefined; v: V | undefined }

function pure<V>(v: V): Either<undefined, V> { return { e: undefined, v }; }
function fail<E>(e: E): Either<E, undefined> { return { e, v: undefined }; }

function f(x: Either<string, number>) {}

const x = pure(1);  // Either<undefined, number>
f(x);  // Ok

const xx = !!true ? pure(42) : fail("string");  // Either<undefined, number> | Either<string, undefined>
f(xx);  // Ok

@wereHamster
Copy link
Author

const x: Either<E,number> = pure(1); // what is E? TypeScript needs to know.

TypeScript is allowed to infer it as any or undefined here. But then when I use x as the first argument of the bind function it should know that that any must be the same type as the E of the function's return type and the return type of the second argument (the f callback). And when I make the return type of bind rigid at the call site, that information should propagate backwards and forwards to any type parameter which is related, and if those don't unify then it should be an error.

@wereHamster
Copy link
Author

Is the following code caused by the same underlying issue or is this something that is reasonably easy to add?

function f<T>(): T { return null;}
function g<T>(x: T): void {}

g<string>(f()); // argument of type '{}' not assignable to parameter of type 'string'

@wereHamster
Copy link
Author

I have an even simpler example:

function f<T>(): T { return null; }
const x: string = f(); // same error

@sandersn
Copy link
Member

sandersn commented Jun 3, 2016

Yes, this is exactly how type parameter inference works in typescript. It only proceeds from function arguments, so a function with no arguments will just default to {}.

Unfortunately, {} and any are both concrete types, not fresh types that will later widen to or unify with some other type from context. Once you have {}, you're stuck with it. And any is even worse. It often infects other expressions.

I believe this architecture was chosen for locality of errors in an incremental context (intellisense, basically) and similarity to existing systems that work the same way (C# and Java, basically). @ahejlsberg can comment more about architectural decisions if you're curious.

@mhegazy mhegazy closed this as completed Jun 7, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

4 participants