Typescript: Enable type parameter lower-bound syntax

Created on 7 Mar 2017  Â·  24Comments  Â·  Source: microsoft/TypeScript



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).

In Discussion Suggestion

Most helpful comment

@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).

All 24 comments

What is this useful for?

@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).

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

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]; }
}

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>?

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 });

<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.

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

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

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

@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: {} });

@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.)

@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)

@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.

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?

I really like @HerringtonDarkholme's example in https://github.com/Microsoft/TypeScript/issues/14520#issuecomment-285295490. 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 
}

@ethanresnick note that making the indexOf method too flexible can be dangerous and lead to surprising type checking behavior.

We can see that in Scala, where indexOf has the type you propose, as shown in this REPL session:

Welcome to the Ammonite Repl 1.6.7
(Scala 2.12.8 Java 1.8.0_121)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ Array(1,2,3).indexOf
def indexOf[B >: A](elem: B): Int             def indexOf[B >: A](elem: B,from: Int): Int
@ Array(1,2,3).indexOf("oops")
res0: Int = -1

Here, the problem is that Scala manages to type-check without errors the expression Array(1,2,3).indexOf("oops") by picking the type parameter B as Any and then using the fact that Any is a supertype of String; i.e., it infers Array[Int](1,2,3).indexOf[Any]("oops").

But it would be more useful to get an error in this case.

If I understand this correctly, what @ethanresnick has proposed would essentially set the parameter type for indexOf to any. Not literally, but essentially because any argument of any type could be passed to indexOf resulting in an infinite number of possible types.

@LPTK mentions the problem with being too flexible, but what about being too restrictive?
Array<2 | 3>[2, 3].indexOf(<1 | 2 | 3>n)?
This is a valid use case that presents a compiler error.

The only _real_ solution in this case would be to test for any type overlap between the array type union and arg type union. But that may not be easy.

Is typecasting as any the best solution to over restrictive type checking in indexOf, or does anyone have a better solution?

I think exposing some notion of whether two types overlap could be quite interesting as a separate feature request, but it's kinda distinct from this issue about bounds. Not sure what to say about indexOf absent that...

Hi all.
I use as as temporary solution

const anyString = 'asdasdasdas';
const TAG_TYPES: Array<TTags> = ['p', 'h1', 'h2', ....];
(TAG_TYPES as Array<string>).indexOf(anyString) === -1;

I think I would like to add here base type. Probably explicitly.
How about adding something like:

TAG_TYPES.indexOf<string>(anyString)

or adding some algorithm to find base type implicitly(probably there is one)?

Conditional types make it possible to emulate this for primitives, at least.

interface Array<T> {
  includes(
    searchElement: T extends string
      ? string
      : T extends number
      ? number
      : T extends boolean
      ? boolean
      : T,
    fromIndex?: number,
  ): boolean;

  indexOf(
    searchElement: T extends string
      ? string
      : T extends number
      ? number
      : T extends boolean
      ? boolean
      : T,
    fromIndex?: number,
  ): number;
}

Is typecasting as any the best solution to over restrictive type checking in indexOf, or does anyone have a better solution?

I hope you find this example helpful: https://github.com/microsoft/TypeScript/issues/26255#issuecomment-515935406

UPD: works only with “strictNullChecks” -> false

Also, it’s possible to overload existing signature globally

Not sure it was mentionaed before, such bound may be useful in orm-like things, where you want to allow selecting some subset of given entity properties, like such:

class User {
  id: number;
  name: string;
  birthday: Date;
  photos: Photo[];
}

const userNames = getRepository(User).select({ name: true }).where(...).getAll();

You want select to accept object (lets call it "shape") with only properties, that entity(User in our case) has. But you cannot restrict it in this way, because extends allows shape to has extra properties. With super you can restrict passed shape in such way:

type ShapeOf<T extends object> = {
  [P in keyof T]: true | false;
};

type OnlyShape<E extends object, S extends object> = {
  [P in keyof E]: P extends keyof S ? S[P] extends true ? E[P] : never : never;
};

class Repository<Entity, Selected> {
  select<Shape super ShapeOf<Entity>>(shape: Shape): Repository<Entity, OnlyShape<Entity, Shape>> {...}
  getAll(): Selected[] {...}
}

so extra properties are prohibited. You may achieve this currently using very hacky method,
when you calculate Shape type using Shape itself:

type ShapeOf<E, S> = {
  [P in keyof S]: P extends keyof E ? S[P] extends boolean ? S[P] : never : never;
}
...
  select<Shape extends ShapeOf<Entity, Shape>>(shape: Shape): Repository<Entity, OnlyShape<Entity, Shape>> {...}
...
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Zlatkovsky picture Zlatkovsky  Â·  3Comments

jbondc picture jbondc  Â·  3Comments

kyasbal-1994 picture kyasbal-1994  Â·  3Comments

Roam-Cooper picture Roam-Cooper  Â·  3Comments

MartynasZilinskas picture MartynasZilinskas  Â·  3Comments