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).
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 Cat
s,
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 Animal
s 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 Animal
s as input. I can just pass two Cat
s 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>> {...}
...
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:The following code shows that the class
Cat
hasAnimal
as its super-class:Now we can write a sorting function that supports arbitrary
Cat
comparators as follows:Now, we will try to use the
sort
function. The first thing is to implement aCatComparator
:Then we create a list of
Cat
s,Now we can call
sort
as follows without any problem:We have not seen the need for contravariance so far.
Now, suppose we are told that someone has already implemented a comparator for
Animal
s as follows:Since a
Cat
is also anAnimal
, thisAnimalComparator
is also able to handleCats
, because thecompare
function inAnimalComparator
takes twoAnimal
s as input. I can just pass twoCat
s to it and there will be no problem.Naturally, we would want to use
AnimalComparator
forsort
too, i.e., call thesort
function as: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 followingor as in Java,
I am aware of the fact that TypeScript does not complain if I pass
AnimalComparator
tosort
. 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).