microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.58k stars 12.43k forks source link

Weird type-widening issue when using methods instead of function properties. #6309

Closed srijs closed 8 years ago

srijs commented 8 years ago

Hi there.

I'm using the tsc compiler, version 1.8.0-dev.20151231, and I'm running into a weird issue where widening of structural types works when I'm defining a class with a function property, and fails when I'm replacing that function property by a "proper" method.

But see for yourself: This is the version that compiles, and this is the version that doesn't.

What is the difference here? Is this a bug in the type inference engine?

DanielRosenwasser commented 8 years ago

This doesn't have to do with widening.

The problem is that you typed run as (T) => X which is _actually_ (T: any) => X. Parameter names are required in function type literals. You can catch this issue by using the --noImplicitAny compiler option.

You'll see the version that errors the same way here.

srijs commented 8 years ago

Thanks, I enabled --noImplicitAny for building my project now.

Out of interest, is there a way I can make this program typecheck without any? Why is TypeScript not able to widen {foo: number} contra-variantly to {foo: number, bar: number} to make it assignable?

For example, I can add explicit type annotations to make it work, like in this version, but could tsc not infer that itself based on the return type of the fooAndBarEq function?

DanielRosenwasser commented 8 years ago

Part of it is the way we select inference points, and the flow of direction between types. Supporting many different inference sites can be complex.

One way to get what you want is to make fooIsEq and barIsEq generic. That way you can say ahead of time what type you're working with.

function fooIsEq<T extends { foo: number }>(x: number) {
  return new Predicate<T, boolean>(v => v.foo === x);
}

function barIsEq<T extends { bar: number }>(x: number) {
  return new Predicate<T, boolean>(v => v.bar === x);
}

function fooAndBarEq(x: number) {
  return fooIsEq<{foo: number; bar: number }>(42)
            .chain(() => barIsEq(42));
}

Another approach is to say that chain can build types up with an intersection type:

class Predicate<T, X> {
  constructor(private _run: (x: T) => X) {}

  public run: (x: T) => X = (v: T) => {
    return this._run(v);
  }

  public chain<U, Y>(f: (x: X) => Predicate<U, Y>) {
    return new Predicate((v: U & T) => f(this.run(v)).run(v));
  }
}

function fooIsEq(x: number) {
  return new Predicate<{foo: number}, boolean>(v => v.foo === x);
}

function barIsEq(x: number) {
  return new Predicate<{bar: number}, boolean>(v => v.bar === x);
}

function fooAndBarEq(x: number) {
  return fooIsEq(42).chain(() => barIsEq(42));
}

This will allow you to compose predicate functions easily, but you're more prone to accidentally passing in an incorrectly typed function.

By the way, does Predicate really need two type parameters? Usually they just return booleans.

srijs commented 8 years ago

Thanks a lot. The & intersection type is pretty cool. Tbh, I was looking for a dual to | previously, but didn't find anything. Is there documentation in the current TypeScript spec for it?

I'd be especially interested in how co & contravariance are solved and how it interacts with the bivariance of function arguments. Is that why you were saying "[I was] more prone to accidentally passing in an incorrectly typed function." with this approach?

DanielRosenwasser commented 8 years ago

We're in the process of documenting things a little more, and the spec will have it soon as well.

What I meant is that you can chain with basically any single-argument function. I guess in practice it won't be a problem, since you'll get an error when you actually try to run with some value that isn't compatible.