Skip to content

Better support for type bounds in generics #13337

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
jklmli opened this issue Jan 7, 2017 · 11 comments
Closed

Better support for type bounds in generics #13337

jklmli opened this issue Jan 7, 2017 · 11 comments

Comments

@jklmli
Copy link

jklmli commented Jan 7, 2017

This is currently valid, legal TypeScript:

class Container<A> {
  constructor(private value: A) {}

  act<B extends A>(b: B) { /*...*/ }
}

But reversing the type bound in act doesn't work, i.e.

act<A extends B>(b: B) { /*...*/ }

is not valid.

Ideally, the class's generic parameter can be used in any position.


Alternatively, we could support lower type bounds through a new keyword, narrows:

act<B narrows A>(b: B) { /*...*/ }

(meaning, B is a supertype of A).

Another possible alternative syntax:

act<B where { A extends B }>(b: B) { /*...*/ }

Examples of implementations in other languages:
http://rustbyexample.com/generics/where.html
http://docs.scala-lang.org/tutorials/tour/lower-type-bounds.html

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 7, 2017

It would definitely be great to enable this scenario to be correctly typed correctly

const xs = [{ a: 'A' }];
const ys = [{ a: 'A', b: 'B' }];

const zs = ys.concat(xs);

currently this is an error because xs is not assignable to ys. However, since a new array is returned and this new array can be safely treated as having the type typeof xs it would be nice to allow it.
EDIT:
This is perfectly possible today with

declare global {
  interface Array<T> {
    concat<A extends B, B>(this: A[], ...items: B[][]): B[];
    concat<A extends B, B>(this: A[], ...items: (B | B[])[]): B[];
  }
}

no new features required.

The problem with removing the current error is that it drastically changes the meaning of existing code where a type parameter is being shadowed

class Container<A> {
  constructor(value: A) { }

  act<A extends B, B>(b: B) { /*...*/ }
}

I'm not sure what the right notation is but where seems to read intuitively.

See also #1394.

@akarzazi
Copy link

akarzazi commented Jan 7, 2017

@jiaweihli can you please share the real code behind this need ?
I wonder how can this be useful.

@aluanhaddad
const zs = xs.concat(ys); is more suitable.

@aluanhaddad
Copy link
Contributor

@akarzazi correct, but they do not mean the same thing.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 7, 2017

I just realized that I can already accomplish what I want, trivially, with the following

declare global {
  interface Array<T> {
    concat<A extends B, B>(this: A[], ...items: B[][]): B[];
    concat<A extends B, B>(this: A[], ...items: (B | B[])[]): B[];
  }
}

@jiaweihli I believe you can do the same:

class Container<A> {
  constructor(private value: A) { }

  act<A extends B, B>(this: Container<A>, b: B) { /*...*/ }
}

@akarzazi
Copy link

akarzazi commented Jan 7, 2017

@aluanhaddad you mean:

  interface Array<T> {
    concat<T extends B, B>(this: T[], ...items: B[][]): B[];
    concat<T extends B, B>(this: T[], ...items: (B | B[])[]): B[];
  }

Edit:

  interface Array<T> {
    concat<T extends B, B>(...items: B[]): B[];
    concat<T extends B, B>(...items: (B | B[])[]): B[];
  }

@aluanhaddad
Copy link
Contributor

@akarzazi it doesn't actually matter if the type parameter name is A or T because the intoduced T is shadowing the T from Array<T> so the this type is being annotated with a new type parameter either way.

@akarzazi
Copy link

akarzazi commented Jan 7, 2017

@aluanhaddad see edit

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 7, 2017

I'm not sure I follow you. The definitions without a this type don't work correctly in several scenarios.

const zs = ys.concat(xs);

that gives zs the type { a: string }[][] but { a: string }[] is what it should be.
But this declaration

interface Array<T> {
    concat<T extends U, U>(this: T[], ...items: U[][]): U[];
    concat<T extends U, U>(this: T[], ...items: (U | U[])[]): U[];
}

infers the lower bound. Note that the method level T shadows the outer type parameter.

@akarzazi
Copy link

akarzazi commented Jan 8, 2017

@aluanhaddad Sorry I didn't open VS to check.

interface Array<T> {
    add2<T extends B, B, C>(...items: B[]): B[];
    add3<A extends B, B>(this: A[],...items: B[]): B[];
  }

interface Type { 
    a: string;
}

interface SubType  extends Type { 
    b: string;
}

var a: SubType[] = [];

let baseTypeInst: Type = { a: "a" };
let notBaseTypeInst: Event;
a.add2(baseTypeInst); // ok 
a.add2(notBaseTypeInst); // No error!

a.add3(notBaseTypeInst); // error: subtype is not assignable to the type Event

It seems that T in the method shadows the outer T. (As you said)

It's worth mentioning that in C# behaves similarly

    public class MyClass<T> where T : ICloneable
    {
        public void Do<T>(T param)  //Warning	CS0693	
        {
            param.Clone(); // Clone is not defined on param
        }
    }

   public interface IMyClass<T> where T : ICloneable
    {
        void Do<T>(T param); //Warning	CS0693	
    }

but there is a warning :

Warning CS0693 Type parameter 'T' has the same name as the type parameter from outer type 'MyClass'

@jklmli
Copy link
Author

jklmli commented Jan 8, 2017

@aluanhaddad Yup, the code you posted sets lower bounds correctly.

@jklmli jklmli closed this as completed Jan 8, 2017
@jyuhuan
Copy link

jyuhuan commented Mar 7, 2017

@aluanhaddad I have a follow up question. Consider the following function in some generic class,

foo<A extends B, B>(b: B) { /* ... */ }

As your code above suggested, this ensures that B is a superclass of A.

But what if A is not a type parameter. What if it is some actual class that is already defined? For example, if I already have the following classes

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

What should I do if I want to enforce that B should be a super class of Kitten? Clearly writing

foo<Kitten extends B, B>(b: B) { /* ... */ }

won't work because this is trying to declare a new type parameter named Kitten, instead of using the class Kitten.

jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
`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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
`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
jklmli added a commit to jklmli/monapt that referenced this issue Jul 5, 2017
`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
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants