-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Would generic type defaults be sufficient to solve this? You would then be able to move 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 |
@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 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. |
👎 for |
When you say "implicit", are you referring to the that for let v1: C<A>; the type is reported by TypeScript services as the string 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 class C<T extends I<X>> When hovering over a variable of type C<A extends I<string>> |
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. On Wed, Apr 6, 2016, 11:56 Paul Jolly [email protected] wrote:
|
@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 Am I missing something? Let's re-write this without omitting 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? |
@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 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. |
@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 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;
}
Apologies if I'm missing something again... |
@isiahmeadows ignore my last message, I now see what you're referring to. This does seem like an issue... |
@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 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, 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. |
@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 // 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)
} |
Note: I do feel there should be a constraint that for // 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
}
} |
@isiahmeadows
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.
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. |
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 |
@RyanCavanaugh - apologies, this is on the back-burner from my perspective at the moment. But I will get round to responding. |
Question: how would this relate to #1213? They're very similar, although about the other facet of higher order kinds. |
@isiahmeadows I believe, that the proposal is a form of type pattern matching. I will comment on this today/tomorrow. It's very interesting. |
@Artazor It kind of is, but it's more like typed destructuring than pattern matching. |
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 |
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? |
@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>>; |
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.
As you can see above, we need to list
X
as a binding identifier ofC
in order to use it as part of the constraint for the binding identifierT
. This leads to the rather clumsy declarations ofv1
andv2
.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 forT
.Any thoughts gratefully received.
The text was updated successfully, but these errors were encountered: