Skip to content
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

Weird type inference / defaulting behavior #11112

Closed
Ptival opened this issue Sep 23, 2016 · 6 comments
Closed

Weird type inference / defaulting behavior #11112

Ptival opened this issue Sep 23, 2016 · 6 comments

Comments

@Ptival
Copy link

Ptival commented Sep 23, 2016

TypeScript Version: nightly (2.1.0-dev.20160915)

Code

declare class Maybe<T> {
  static nothing<T>(): Maybe<T>;
//   valueOrThrow(error?: Error): T;
}

function muchAdoAbout(m: Maybe<string>): void {}
muchAdoAbout(Maybe.nothing())

Expected behavior:
When the commented out line is uncommented, the code should still type-check (the nothing on the last line should get its type found to be string by use-site inference like it does already).

Actual behavior:
When you uncomment the valueOrThrow line, the type inferred at the call site becomes {} and the code does not type-check anymore.

I'm not sure why adding that line has this effect. Note that it works if you do instead:

//   valueOrThrow<T>(error?: Error): T;

But I'd like to understand if the behavior shown here is a bug or intended...

Not sure if related to:
#5254

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Sep 24, 2016

This is correct behavior. To understand why lets consider the declaration

declare class Maybe<T> {
  static nothing<T>(): Maybe<T>;
  valueOrThrow(error?: Error): T;
}

This declares a class. Declaring a class creates two types and a value.

type Maybe<T> = { 
  valueOrThrow(error?: Error): T; 
};

type MaybeConstructor = { 
  new<T>(): Maybe<T>;
  nothing<T>(): Maybe<T>;
};

declare let Maybe: MaybeConstructor;

Note where the type arguments appear in the decomposition above.
Specifically, a value of type Maybe<T> has a callable member valueOrThrow, that returns a value of type T where T is the T for that Maybe<T>.
A value of type MaybeConstructor has:

  1. a callable member new<T>() that takes a single type argument, T, and returns a value of type Maybe<T>
  2. a callable member nothing<T>() that takes a single type argument, T, and returns a value of type Maybe<T>
  3. The identifier Maybe has two different meanings

a. The generic type Maybe<T> which is the type of values of type { valueOrThrow(error?: Error): T }

b. The value Maybe which has the the non generic type MaybeConstructor having the members new<T>(): Maybe<T> and nothing<T>(): Maybe<T>.

In summary, Maybe refers to the value Maybe of type MaybeConstructor and the type Maybe which, given a type argument, T, represents the types of the values of created by calling the members of the value Maybe.
Note that if you peruse lib.d.ts you will see that arrays and many others are specifically declared in this manner but the suffix Static is used where I have used Constructor.

EDIT:
Note the correct code is

muchAdoAbout(Maybe.nothing<string>());

@yortus
Copy link
Contributor

yortus commented Sep 24, 2016

I agree with @aluanhaddad it's correct behaviour. I think it's even simpler to explain.

With the valueOrThrow line commented out, Maybe<T> instances have no properties, so Maybe<string> is just the same as {}, which is the supertype of every other type. So TypeScript allows you to pass literally anything to muchAdoAbout, including a Maybe<{}>, a RegExp, a different class, or whatever.

But with the valueOrThrow method present, Maybe<string> and Maybe<{}> now have distinct shapes so the compiler doesn't allow the muchAdoAbout call with the incompatible type.

@aluanhaddad
Copy link
Contributor

@yortus that is indeed true. I was being extra specific because I believe the OP intended for there to be an instance member but had commented it out in the course of trial and error debugging.

@yortus
Copy link
Contributor

yortus commented Sep 24, 2016

@aluanhaddad fair enough. @Ptival note that Maybe.nothing() is inferred as Maybe<{}> regardless whether the other line is commented out or not (you can check intellisense).

@Ptival
Copy link
Author

Ptival commented Sep 24, 2016

Hmmm I'm still not quite convinced. What makes this work until you uncomment one of the three lines?

declare class Beep<T> { beepField: string }
declare class Boop<T> { boopField: T }

declare class Maybe<T> {
    static nothing<T>(): Maybe<T>
    foo(t: T): any // fine as a parameter
    bar(): Maybe<T> // fine in some return types
    beep: Beep<T> // fine in some return types
    // bad: T // bad
    // baz: T[] // bad
    // boop: Boop<T> // bad
}

function muchAdoAbout(m: Maybe<string>): void { }
muchAdoAbout(Maybe.nothing())

It seems that it matters in what position/variance T appears.

@aluanhaddad
Copy link
Contributor

@Ptival This goes back to @yortus's remark

With the valueOrThrow line commented out, Maybe instances have no properties, so Maybe is just the same as {}, which is the supertype of every other type. So TypeScript allows you to pass literally anything to muchAdoAbout, including a Maybe<{}>, a RegExp, a different class, or whatever.

Note that

declare class Beep<T> { beepField: string }

does not use its type argument but

declare class Boop<T> { boopField: T }

does.
If you use Beep<T> anywhere it will always be compatible with Beep<string>
now given

declare class Maybe<T> {
    static nothing<T>(): Maybe<T>;
    getValue (): T; // error
    setValue(x: T): any; // not an error
}

function muchAdoAbout(m: Maybe<string>): void {
  m.setValue("abc");
}
muchAdoAbout(Maybe.nothing());

It is a little more complicated. Having setValue is acceptable because whatever the actual type of the argument, it is a subtype of {}. having getValue introduces an error because it must produce a string but Maybe.nothing() produces a value with a getValue method that returns something of type {} which is not a subtype of string.

@Ptival Ptival closed this as completed Sep 24, 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
None yet
Projects
None yet
Development

No branches or pull requests

3 participants