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

Typing subclass-factory-style mixin functions #7225

Closed
justinfagnani opened this issue Feb 24, 2016 · 2 comments
Closed

Typing subclass-factory-style mixin functions #7225

justinfagnani opened this issue Feb 24, 2016 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@justinfagnani
Copy link

Apologies in advance, as this might be more of a question, but I'm asking here rather than StackOverflow because there might be some changes to the type checking that could allow this pattern to be easier to express, and I might be encountering some bugs.

I'm trying to convert the subclass factory mixin pattern to TS, with enough type info to be able to determine the complete object shape of a class that uses these mixins. With intersection types and f-bounded quantification, it seems like we should be in really good shape, but I ran into several difficulties and maybe some bugs.

TypeScript Version:

1.8.0

Code

Here's the untyped JS:

let M1 = (superclass) => class extends superclass {
  foo() { return 'a string'; }
}

let M2 = (superclass) => class extends superclass {
  bar() { return 42; }
}

class C = M2(M1(Object)) {
  baz() { return true; }
}

let c = new C();
console.log(c.foo(), c.bar(), c.baz());

When converting to TypeScript, the first issue is that class extends superclass complains that superclass is not a constructor function type, so I introduce an interface for that:

interface Constructable {
  new (): Object;
}

let M1 = (superclass: Constructable) => class extends superclass {
  foo() { return 'a string'; }
}

let M2 = (superclass: Constructable) => class extends superclass {
  bar() { return 42; }
}

This causes the mixin declarations to pass type checking, but the usage loses type information for nested mixins:

console.log(c.foo(), c.bar(), c.baz()); // Property 'foo' does not exist on type 'C1'.

With intersection types, I hope to be able to type the subclass factories such that they include both the superclass and class they declare. Something like:

let M1 = <T>(superclass: Constructable<T>): T & M1 => class extends superclass {
  foo() { return 'a string'; }
}

Obviously I can't reference M1 like this because it refers to the class factory, not the type that the factory returns. I can remove it though:

let M1 = <T>(superclass: Constructable<T>): T => class extends superclass {
  foo: string;
}

and now I get the error: TS2322: Type 'typeof (Anonymous class)' is not assignable to type 'T'.

Which makes sense, because superclass is a Constructable<T>, not a T, which should be solvable by the new support for f-bounded quantification:

interface Constructable<T extends Constructable<T>> {
  new (): T;
}

let M1 = <T extends Constructable<T>>(superclass: T): T => class extends superclass {
  foo: string;
}

But now I get another error on extends superclass: TS2507: Type 'T' is not a constructor function type. even though T should implement Constructable<T> which is a constructor function type. Is this a bug?

Another possible bug I ran into is with my attempt at the Constructable interface. As I mentioned, I can get the declaration to (questionably) pass type checking, before using recursive constraints:

interface Constructable<T> {
  new (): Object;
}

let M1 = <T>(superclass: Constructable<T>) => class extends superclass {
  foo() { return 'a string'; }
}

but the declaration for new is off, it should be:

interface Constructable<T> {
  new (): T;
}

But this triggers the error: TS2509: Base constructor return type 'T' is not a class or interface type.

neither of these variants fix it:

interface Constructable<T extends Object> {
  new (): T;
}

or:

interface Constructable<T> {
  new (): T & Object;
}

Even once these issues (which might be my fault, I hope!) are over come, there's another problem of being able to refer to the type returned by a subclass factory. It seems like I would have to define an interface as well as the class expression, which is enough duplicate work to make this pattern very cumbersome to use in TypeScript.

Assuming tsc can eventually correctly infer the type returned by a subclass factory M1, it would be great to be able to refer to that type for use in implements, etc.

@DanielRosenwasser
Copy link
Member

I think #4890 is related.

A few notes since it looks like you might be slightly new to TS:

interface Constructable {
  new (): Object;
}

This is unfortunately a common mistake. Object is never actually what you want. You either want any in this instance. Alternatively, you probably want

interface Constructable<T> {
    new(): T;
}

which you've done below.


interface Constructable<T extends Constructable<T>> {
  new (): T;
}
let M1 = <T extends Constructable<T>>(superclass: T): T => class extends superclass {
  foo: string;
}

I think this signature is slightly misguided. This is saying you're passing in a constructor whose constructed type is identical to the type of the constructor. Something like:

interface Foo {
    new(): Foo;
}
let Foo: Foo;

But then you could do this:

new new new new new new Foo(); // totally valid

So that's probably not what you'd want.

@justinfagnani
Copy link
Author

Good points. And I am fairly new to TS. I seem to have mixed up the static and instance side interfaces.

#4890 looks like nearly exactly I'm trying to do. I might be able to close this.

@mhegazy mhegazy closed this as completed Feb 26, 2016
@mhegazy mhegazy added the Duplicate An existing issue was already created label Feb 26, 2016
@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
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants