Skip to content

Typing subclass-factory-style mixin functions #7225

Closed
@justinfagnani

Description

@justinfagnani

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions