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

Shortening generic type parameter lists #7848

Closed
myitcv opened this issue Apr 6, 2016 · 22 comments
Closed

Shortening generic type parameter lists #7848

myitcv opened this issue Apr 6, 2016 · 22 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@myitcv
Copy link

myitcv commented Apr 6, 2016

Tested in >= v1.8

Would very much appreciate some feedback here on whether a) we're missing something obvious or b) there is an opportunity to improve generic type/function declarations in some way.

interface I<T> {
    P1: T;
}

class C<T extends I<X>, X> {
    P1: X;
    P2: T;
}

class A {
    P1: string;
}

let v1: C<A, string>;             // clumsy looking definition
let v2: C<C<A, string>, string>;  // it gets worse...

As you can see above, we need to list X as a binding identifier of C in order to use it as part of the constraint for the binding identifier T. This leads to the rather clumsy declarations of v1 and v2.

Is there a way to achieve the above without needing to separately list X? I suspect not given what I've read in the spec... hence...

The following does not compile but is essentially equivalent. X is effectively implied as a binding identifier by virtue of appearing as part of the constraint for T.

// does not compile
interface I<T> {
    P1: T;
}

class C<T extends I<X>> {
    P1: X;
    P2: T;
}

class A {
    P1: string;
}

let v1: C<A>;    // much neater
let v2: C<C<A>>; // much neater

Any thoughts gratefully received.

@RyanCavanaugh
Copy link
Member

Would generic type defaults be sufficient to solve this?

You would then be able to move X to be the primary type parameter and write something like

class C<X, T extends I<X> = I<X>> { /* same as current*/ }

let c1: C<string>;
let c2: C<C<string>>;

I'm not 100% clear on your use case so feel free to say if this is way off the mark

@myitcv
Copy link
Author

myitcv commented Apr 6, 2016

@RyanCavanaugh thanks for the quick (late night in Seattle?) response!

Hmm, what you propose is an interesting corollary of generic type defaults. But I don't think it works in our situation.

I've rewritten our example as follows:

interface HasKey<T> {
    Key(): T;
}

class Row<T extends HasKey<X>, X> {
    private t: T;

    constructor(t: T) {
        this.t = t; 
    }

    Key(): X {
        return 
    }

    V0(): T {
        return this.t;
    }
}

class MyModel {
    Key(): string {
        return "test";
    }

    Name(): string {
        return "Paul";
    }
}

let v1: Row<MyModel, string>;             
let v2: Row<Row<MyModel, string>, string>;  

console.log(v1.V0().Name());
console.log(v2.V0().V0().Name());

With your approach the two console.log lines would fail to compile because the type of V0 would be I<string> (unless I'm missing something)

So in summary, I don't think this is a defaulting "issue", rather one about automatically declaring (that's the wrong word but I don't know the correct term) binding identifiers in a generic type/function specification if they form part of the constrain of another binding identifier.

@dead-claudia
Copy link

👎 for T extends I<X> without anything defining X. It's too implicit IMHO, and autocomplete would be immediately less helpful because the parameter is now very implicit. I'd have less of a problem in, say, Haskell or OCaml, where parameters are always implicitly defined (the algorithm rarely needs explicit annotations), and the idioms make generic parameters a whole different case so it's easier to tell, but TypeScript idioms and syntax would obfuscate that.

@myitcv
Copy link
Author

myitcv commented Apr 6, 2016

It's too implicit IMHO and autocomplete would be immediately less helpful because the parameter is now very implicit

When you say "implicit", are you referring to the that for v1:

let v1: C<A>;

the type is reported by TypeScript services as the string C<A>?

If so then I agree, but I think that's easily addressable.

For autocompletion, I would expect the user to see the following information for C:

class C<T extends I<X>>

When hovering over a variable of type C<T extends I<X>>, e.g. v1, the user would see:

C<A extends I<string>>

@dead-claudia
Copy link

Yeah, but what about something like this:

class Foo<Type1> {
  bar<U extends Bar<Child>>(x: U): Baz<Type, U> {
    // ...
  }
}

That kind of code is a very realistic example of what you might expect.
What if someone creates a new type that Child perfectly fits? Then, you
have to refactor the function to not use that type. It's too brittle IMO,
and even decent code can run into problems like that.

On Wed, Apr 6, 2016, 11:56 Paul Jolly [email protected] wrote:

It's too implicit IMHO and autocomplete would be immediately less helpful
because the parameter is now very implicit

When you say "implicit", are you referring to the that for v1:

let v1: C;

the type is reported by TypeScript services as the string C?

If so then I agree, but I think that's easily addressable.

For autocompletion, I would expect the user to see the following
information for C:

class C<T extends I>

When hovering over a variable of type C<T extends I>, e.g. v1, the
user would see:

C<A extends I>


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
#7848 (comment)

@myitcv
Copy link
Author

myitcv commented Apr 6, 2016

@isiahmeadows

I don't understand the point you're getting at in your example:

class Foo<Type1> {
  bar<U extends Bar<Child>>(x: U): Baz<Type, U> {
    // ...
  }
}

You haven't used the Child type parameter here, and in any case Child is not constrained.

Am I missing something?

Let's re-write this without omitting Child:

class Foo<Type1> {
  bar<U extends Bar<Child>, Child>(x: U): Baz<Type, U> {
    // ...
  }
}

What does this give you that the proposed version does not?

@dead-claudia
Copy link

@myitcv This:

// Old
class Foo<Type> {
    coerce<T extends Bar<Child>>(bar: T): Item<Type, T> {
        // ...
    }

    append(foo: ChildNode<Type>): void {
      // ...
    }
}

// New
type Child = ViewChild<Person, any>;

class Foo<Type> {
    coerce<T extends Bar<Child>>(bar: T): Item<Type, T> {
        // ...
    }

    append(foo: ChildNode<Type, Child>): void {
      // ...
    }
}

What does Child in coerce refer to now? It should refer to the outer Child now (otherwise, it would break a lot of code), but it previously declared a generic type variable. That context sensitivity is pretty dangerous IMHO.

Here's a diff of all that changed:

+ type Child = ViewChild<Person, any>;
+
  class Foo<Type> {
      coerce<T extends Bar<Child>>(bar: T): Item<Type, T> {
          // ...
      }

-     append(foo: ChildNode<Type>): void {
+     append(foo: ChildNode<Type, Child>): void {
        // ...
      }
  }

Multiply that by 500+ lines in a single file, and you should hopefully see why I'm not too happy with this idea.

@myitcv
Copy link
Author

myitcv commented Apr 7, 2016

@isiahmeadows I'm not proposing any change to the scope of types.

Here is a rough attempt to make your example compile using today's TypeScript implementation (playground link):

// compiled with TS 1.8
interface Bar<T> {}
class Item<T1, T2> {}
class ChildNode<T> {}
class ViewChild<T1, T2> {};
class Person {};

type Child = ViewChild<Person, any>;

class Foo<Type> {
    coerce<T extends Bar<Child>, Child>(bar: T): Item<Type, T> {
        let x: Child; // Child the type parameter
        return undefined;
    }

    append(foo: ChildNode<Type>): void {
        let x: Child; // ViewChild<Person, any>
    }
}

Your question about what Child in coerce refers to presumably exists in the above code too, correct?

In the context of this proposal:

// does not compile
interface Bar<T> {}
class Item<T1, T2> {}
class ChildNode<T> {}
class ViewChild<T1, T2> {};
class Person {};

type Child = ViewChild<Person, any>;

class Foo<Type> {
    coerce<T extends Bar<Child>>(bar: T): Item<Type, T> {
        let x: Child; // Child the type parameter
        return undefined;
    }

    append(foo: ChildNode<Type>): void {
        let x: Child; // ViewChild<Person, any>
    }
}

The diff:

@@ -7,7 +7,7 @@
 type Child = ViewChild<Person, any>;

 class Foo<Type> {
-    coerce<T extends Bar<Child>, Child>(bar: T): Item<Type, T> {
+    coerce<T extends Bar<Child>>(bar: T): Item<Type, T> {
         let x: Child; // Child the type parameter
         return undefined;
     }

To reiterate the proposal:

class C<T extends I<X>, X> {
    P1: X;
    P2: T;
}

// would become ->

class C<T extends I<X>> {
    P1: X;
    P2: T;
}

X has exactly the same scope as before. Equally, if there is any ambiguity in today's implementation (c.f. the example above) that would neither improve nor get worse under this proposal.

Apologies if I'm missing something again...

@myitcv
Copy link
Author

myitcv commented Apr 7, 2016

@isiahmeadows ignore my last message, I now see what you're referring to. This does seem like an issue...

@myitcv
Copy link
Author

myitcv commented Apr 7, 2016

@isiahmeadows thanks for spotting the problem with the initial proposal

The following adjusted proposal solves the aforementioned scope problem whilst still allowing for the shortened form of declaration:

// does not compile
interface I<T> {
    P1: T;
}

class C<T extends I<let X>> {
    P1: X;
    P2: T;
}

class A {
    P1: string;
}

let v1: C<A>;    // much neater
let v2: C<C<A>>; // much neater

Not fixed on using let here, but it seems most appropriate given the scoping rules associated with the keyword, that is if we were looking to avoid introducing another keyword (which seems desirable)

Just to give some more colour on why we see this as a benefit.

Recall the current state of affairs:

interface I<T> {
    P1: T;
}

class C<T extends I<X>, X> {
    P1: X;
    P2: T;
}

class A {
    P1: string;
}

let v1: C<A, string>; 

In our situation, A is such that A.P1 is a type of some complexity, for example Pair<String, String>. Given the constraint on C, listing the type for X is only providing redundant information.

In such situations we quickly get to the point where we have declarations like the following:

class Pair<T0, T1> {
    // ....
}

class B {
    P1: Pair<String, String>;
}

let x: C<C<B, Pair<String, String>>, Pair<String, String>>; // totally unreadable, and this isn't the most complex type

which under this proposal would simply collapse down to:

let x: C<C<B>>;

Any further thoughts gratefully received.

@dead-claudia
Copy link

@myitcv Outside declaration files, would TypeScript's type inference help this one? That seems IMHO a case where TypeScript's type inference really shows its usefulness (really complex types that don't need to be explicitly written every time).

Also, that doesn't seem as much of a problem to me if you employ a type alias.

class Pair<T0, T1> {
    // ....
}

type P = Pair<string, string>;
class B {
    P1: P;
}

let x: C<C<B, P>, P>;

Keep in mind, I'm not inherently against it at this point, because you have since addressed the initial problem. Although I think this approach would be better, and several languages like Scala already have it:

interface I<T> {
    p1: T;
}

class C<T<X> extends I<X>> {
    p1: X;
    p2: T<X>;
    constructor(p2: T<X>) {
      this.p1 = p2.p1
      this.p2 = p2
    }
}

class A {
    p1: string = "Hi!";
}

// Tiny bit of boilerplate for an atypical case
type A1<T> = A;
let v1: C<A1<any>> = new C(new A())
let v2: C<C<A>> = new C(v1)

The Scala equivalent would be this (although Scala has better ways to handle the problem in class A):

// Not exactly idiomatic
trait I[T] {
    def p1: T
}

class C[+M[+X] <: I[X]](val p2: M[X]) extends I[X] {
    def p1: X = p2.p1
}

class A extends I[_] {
    val p1: string = "Hi!"
}

object Main extends App {
    // Tiny bit of boilerplate for an atypical case
    type A1[_] = A
    val v1: C[A1[_]] = new C(new A)
    val v2: C[C[A]] = new C(v1)
}

@dead-claudia
Copy link

Note: I do feel there should be a constraint that for T<A>, A should be assignable to X, or at the very least, T<X> should be considered the same kind of type as a non-generic Foo. Otherwise, it's 0% type safe.

// This shouldn't type check.
class C<T<X> extends I<X>> {
    p1: X;
    p2: T<X>;
    p2: T<Foo>;
    constructor(p2: T<X>) {
      this.p1 = p2.p1
      this.p2 = p2
    }
}

@myitcv
Copy link
Author

myitcv commented Apr 10, 2016

@isiahmeadows

Outside declaration files, would TypeScript's type inference help this one?

It will, yes, where a variable is initialised. But there still exists situations where one needs to declare variables of the correct type without initialising them. Very common in tests for example.

We could conceivably go down the following path:

class A {
    static get Undef(): A { return undefined; }
}

let x = A.Undef; // x has type A
let y: A;        // y has type A

but, ignoring any sort of optimisation, this is an expensive runtime way to solve a compile time problem.

Also, that doesn't seem as much of a problem to me if you employ a type alias.

This seems to somewhat defeat the point of generics... because had we wanted to go down this path we would already have code generated specific types (we already have a significant amount of code generation) and erased generics in the process.

I'll review your Scala parallel later... thanks for offering a different point of view.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jun 7, 2016
@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed In Discussion Not yet reached consensus labels Jun 8, 2016
@RyanCavanaugh
Copy link
Member

Interesting thread here; the code outlined in #7848 (comment) is getting closer to a proposal we could work from in terms of discussing the feature. If you could write up some more details about what you expect the semantics of this to be (e.g. is X an actual formal generic type parameter, or is it only visible inside the scope of the declaration? how would you declare a constraint on X?) and possibly more (or faster-to-understand) use cases, that'd be great.

@myitcv
Copy link
Author

myitcv commented Jun 30, 2016

@RyanCavanaugh - apologies, this is on the back-burner from my perspective at the moment. But I will get round to responding.

@dead-claudia
Copy link

dead-claudia commented Jul 12, 2016

Question: how would this relate to #1213? They're very similar, although about the other facet of higher order kinds.

@Artazor
Copy link
Contributor

Artazor commented Jul 20, 2016

@isiahmeadows I believe, that the proposal is a form of type pattern matching. I will comment on this today/tomorrow. It's very interesting.

@dead-claudia
Copy link

@Artazor It kind of is, but it's more like typed destructuring than pattern matching.

@bencoveney
Copy link

bencoveney commented Aug 2, 2016

I have another use case if it is of any use to the discussion. I believe this is the same issue but I am getting lost in some of the generics in the examples.

The context is that the classes are models and views in a backbone (+ marionette) system. The reason I have come across this is that there are multiple distinct class hierarchies which are all correlated to each another: models, model views, collections and collection views.

// Hierarchy of individual items.
abstract class Item {}
class RowItem extends Item {}
class ColItem extends Item {}

// Hierarchy of containers.
abstract class Container<TItem extends Item> {
    items: TItem[];
}
class RowContainer extends Container<RowItem> {}
class ColContainer extends Container<ColItem> {}

// Hierarchy of item views.
abstract class ItemView<TItem extends Item> {
    item: TItem;
}
class RowItemView extends ItemView<RowItem> {}
class ColItemView extends ItemView<ColItem> {}

// Hierarchy of container views.                                     /* redundant------ */
abstract class ContainerView<TContainer extends Container<TDesired>, TDesired extends Item> {
    container: TContainer;
    public getItem(index: number): TDesired {
        // Example dummy method which uses desired type.
        return this.container.items[index];
    }
}
class RowContainerView extends ContainerView<RowContainer, RowItem> {}
class ColContainerView extends ContainerView<ColContainer, ColItem> {}

I would expect to not need to specify TDesired as the argument to ContainerView and for intellisense (when hovering on TDesired) to work out that it can only be TDesired extends TItem.

@RyanCavanaugh
Copy link
Member

This is solvable now by using a conditional type to extract the desired type out of the type parameter.

@akessner
Copy link

This is solvable now by using a conditional type to extract the desired type out of the type parameter.

Can you give a syntax example?

@pelotom
Copy link

pelotom commented Jul 21, 2020

@akessner something like this?

interface I<T> {
    P1: T;
}

interface C<T extends I<any>> {
    P1: T extends I<infer X> ? X : never;
    P2: T;
}

interface A {
    P1: string;
}

let v1: C<A>;
let v2: C<C<A>>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants