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

Better support for type bounds in generics #13337

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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants