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

Enable type parameter lower-bound syntax #14520

Open
jyuhuan opened this issue Mar 7, 2017 · 16 comments

Comments

Projects
None yet
10 participants
@jyuhuan
Copy link

commented Mar 7, 2017

TypeScript Version: 2.1.1

Code

class Animal {}
class Cat extends Animal {}
class Kitten extends Cat{}

function foo<A super Kitten>(a: A) { /* */ }

Expected behavior:
The type parameter A has the type Kitten as lower-bound.

Actual behavior:
Compilation failure. The syntax is unsupported.

Discussion:
The upper-bound counterpart of the failed code works fine:

class Animal {}
class Cat extends Animal {}
class Kitten extends Cat{}

function foo<A extends Animal>(a: A) { /* */ }

People in issue #13337 have suggested to use

function foo <X extends Y, Y>(y: Y) { /* */ }

to lower-bound Y with X. But this does not cover the case where X is an actual type (instead of a type parameter).

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented Mar 7, 2017

What is this useful for?

@jyuhuan

This comment has been minimized.

Copy link
Author

commented Mar 7, 2017

@RyanCavanaugh : In short, it mimics contravariance, just as extends mimics covariance.

We will try to sort an array of cats to see the necessity of this feature.

To do comparison-based sorting, we need a Comparator interface. For this example, we define it as follows:

interface Comparator<T> {
  compare(x: T, y: T): number
}

The following code shows that the class Cat has Animal as its super-class:

class Animal {}
class Cat extends Animal {}

Now we can write a sorting function that supports arbitrary Cat comparators as follows:

function sort(cats: Cat[], comparator: Comparator<Cat>): void {
  // Some comparison-based sorting algorithm.
  // The following line uses the comparator to compare two cats.
  comparator.compare(cats[0], cats[1]);
  // ...
}

Now, we will try to use the sort function. The first thing is to implement a CatComparator:

class CatComparator implements Comparator<Cat> {
  compare(x: Cat, y: Cat): number {
    throw new Error('Method not implemented.');
  }
}

Then we create a list of Cats,

const cats = [ new Cat(), new Cat(), new Cat() ]

Now we can call sort as follows without any problem:

sort(cats, new CatComparator());

We have not seen the need for contravariance so far.

Now, suppose we are told that someone has already implemented a comparator for Animals as follows:

class AnimalComparator implements Comparator<Animal> {
  compare(x: Animal, y: Animal): number {
    throw new Error('Method not implemented.');
  }
}

Since a Cat is also an Animal, this AnimalComparator is also able to handle Cats, because the compare function in AnimalComparator takes two Animals as input. I can just pass two Cats to it and there will be no problem.

Naturally, we would want to use AnimalComparator for sort too, i.e., call the sort function as:

sort(cats, new AnimalComparator());

However, since the following two types:

  • Comparator<Animal>
  • Comparator<Cat>

are not related from the point of view of TypeScript's type system, we cannot do that.

Therefore, I would like the sort function to look like the following

function sort<T super Cat>(cats: Cat[], comparator: Comparator<T>): void {
  // Some comparison-based sorting algorithm.
  // The following line uses the comparator to compare two cats.
  comparator.compare(cats[0], cats[1]);
  // ...
}

or as in Java,

function sort(cats: Cat[], comparator: Comparator<? super Cat>): void {
  // Some comparison-based sorting algorithm.
  // The following line uses the comparator to compare two cats.
  comparator.compare(cats[0], cats[1]);
  // ...
}

I am aware of the fact that TypeScript does not complain if I pass AnimalComparator to sort. But I would like TypeScript's type system to explicitly handle type lower-bounds. In fact, the current type system of TypeScript will let some type error closely related to this issue silently pass the compiler's check (see issue #14524).

@jiaweihli

This comment has been minimized.

Copy link

commented Mar 7, 2017

This is the best example of contravariance I've read in a long time. Props! 🙌

@btipling

This comment has been minimized.

Copy link

commented Mar 8, 2017

As a reference point, flowtype uses - and + to indicate covariance and contravariance

class ReadOnlyMap<K, +V> {
  store: { +[k:K]: V };
  constructor(store) { this.store = store; }
  get(k: K): ?V { return this.store[k]; }
}
@HerringtonDarkholme

This comment has been minimized.

Copy link
Contributor

commented Mar 9, 2017

Hi @jyuhuan , since you probably already know this https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant

I'm afraid lower bound isn't that useful in current TypeScript where variance is unsound.

Indeed, there are also cases lower bound can be useful without variance. Like

interface Array<T> {
  concat<U super T>(arg: ): Array<U>
}

var a = [new Cat]
a.concat(new Animal) // inferred as Animal[]

In such case like immutable sequence container, lower bound helps TypeScript to enable pattern where new generic type is wider than original type.

Yet such usage still needs more proposal since TS also has union type. For example should strArr.concat(num) yield a Array<string | number>?

@zpdDG4gta8XKpMCd

This comment has been minimized.

Copy link

commented Mar 20, 2017

migrated from #14728:

currently when we see a constraint <A extends B> it means A is a subtype of B, so that

declare var a: A;
declare var b: B;
b = a; // allowed
a = b; // not allowed

consider adding a new constraint of the reversed relation: <A within B> that would mean A is a supertype of B (or in other words B is subtype of A), so that:

declare var a: A;
declare var b: B;
b = a; // not allowed
a = b; // allowed

use case

i have a BigFatClass with 50 methods and a 1 little property, now i want to run some assertions over it, if i declare these assertions like expect<T>() and toEqual<T> of the same T then toEqual asks me for 50 methods that don't matter for the test, and that's the problem

what i need it to declare expect<T>() and toEqual<U within T>() so that i could simply write:

expect(new BigFatClass()).toEqual({ value: true });
@aaronbeall

This comment has been minimized.

Copy link

commented Mar 20, 2017

<U within keyof T> could be confusing, since U in this case should be a super-set of T keys, not "within" the keys of T.

@zpdDG4gta8XKpMCd

This comment has been minimized.

Copy link

commented Mar 20, 2017

the idea is that U is always a subset, never a superset, so if so it should not be a problem

@aaronbeall

This comment has been minimized.

Copy link

commented Mar 20, 2017

So <U within keyof T> would be the same as <U extends keyof T>?

@zpdDG4gta8XKpMCd

This comment has been minimized.

Copy link

commented Mar 21, 2017

i always forget that subtype of a union is a subset of that union, conversely a supertype of a union must be a superset, so you right that within would look like a poor choice of word to indicate that something is a superset of something else, how about A unties B or A relaxes B or loosens, frees, eases etc: https://www.powerthesaurus.org/loosen/synonyms

@zpdDG4gta8XKpMCd

This comment has been minimized.

Copy link

commented Mar 21, 2017

@aaronbeall problem with Partial<T> for making a supertype of a product type, is that it only works at the top level, so it doesn't really work for my use case, consider:

type Super<T> = Partial<T>;
type Data = { nested: { value: number; }; }
const one: Super<Data> = {}; // works
const another: Super<Data> = { nested: {} }; // bummer

so i am back to hammering the expected values with the type assertions

expect(data).toEqual(<Data>{ nested: {} });
@aaronbeall

This comment has been minimized.

Copy link

commented Mar 21, 2017

@aleksey-bykov Probably doesn't make this feature any less valuable but for your case I think you can use a recursive partial type:

type DeepPartial<T> = {
  [P in keyof T]?: DeepPartial<T[P]>;
}

(This was suggested as an addition to the standard type defs, which I think would be very useful.)

@zpdDG4gta8XKpMCd

This comment has been minimized.

Copy link

commented Mar 22, 2017

@aaronbeall DeepPartial almost works for my needs, except that due to having an empty object in it, it can be assigned by anything (except null and undefined), and it's a problem:

type Super<T> = DeepPartial<T>
type Data = { value: number; }
const one: Data = 5; // not allowed
const another: Super<Data> = null; // not allowed
const yetAnother: Super<Data> = 5; // allowed
const justLike: {} = 5; // <-- reason why (allowed)
@jmlopez-rod

This comment has been minimized.

Copy link

commented May 9, 2017

@aleksey-bykov Woah, you just answered my question. But now I have another one, why is this allowed?

const a1: {} = 0;
const a2: {} = '0';
const a3: {} = false;
const a4: {} = { a: 0 };

Is {} the same asany minus null and undefined?

EDIT: I tried the following DeepPartial definition in the link I provided and it seems to work.

type DeepPartial<T> = {[P in keyof T]?: T[P] | (DeepPartial<T[P]> & object); };

It is pretty late, maybe that won't work either, I'll probably think of some example that will break it once I wake up.

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
`getOrElse` and `orElse` use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like `interface Option<A, B = Option<A, B>>` is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
getOrElse and orElse use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like `interface Option<A, B = Option<A, B>>` is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
getOrElse and orElse use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like interface Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
getOrElse and orElse use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like interface Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
getOrElse and orElse use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like interface Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix: use generic self-types
getOrElse and orElse use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method body.
This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[2], but true F-bounded polymorphism hasn't been.
This means a type like interface Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete classes[3].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#stricter-checking-for-generic-functions
[2] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#type-parameters-as-constraints
[3] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix(*): use generic self-types
`getOrElse` and `orElse` use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method
body.  This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[1], but true F-bounded polymorphism hasn't been.
This means a type like `interface` Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete
classes[2].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors
compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript
[2] microsoft/TypeScript#14520

jiaweihli added a commit to jiaweihli/monapt that referenced this issue Jul 5, 2017

fix(*): use generic self-types
`getOrElse` and `orElse` use self-types in order to support typed upper bounds.[0]

In TypeScript 2.4, generic functions were checked more strictly[1].
This causes the implicit downward type cast to fail, so we explicitly invoke the cast in the method
body.  This workaround is backwards-compatible with TypeScript 2.3.

Bounded polymorphism has been implemented[1], but true F-bounded polymorphism hasn't been.
This means a type like `interface` Option<A, B = Option<A, B>> is invalid.

Alternatively, we can solve this with a lower type bound, but these don't work against concrete
classes[2].

---

We should also upgrade monapt's TypeScript dependency to 2.4, but there are unrelated errors
compiling the tests.

[0] microsoft/TypeScript#13337
[1] https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript
[2] microsoft/TypeScript#14520
@pelotom

This comment has been minimized.

Copy link

commented Apr 22, 2018

Instead of <A super Kitten> or <A within Kitten> or any other new keyword, why not just <Kitten extends A>, i.e. allow putting the new type variable on the right of the extends?

@ethanresnick

This comment has been minimized.

Copy link
Contributor

commented May 9, 2018

I really like @HerringtonDarkholme's example in #14520 (comment). I'd add indexOf as another common, standard library method where this would be useful. That is, if I have:

declare a: string[];
declare b: string | number;

a.indexOf(b); // fails, would be nice for this to succeed without a type assertion on b.

That could happen if the types looked like:

interface Array<T> {
  indexOf<U super T>(arg: U): boolean 
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.