Skip to content

A class can't extend from object/generic intersection, but can extend from plain generic #33127

Closed
@DanielRosenwasser

Description

@DanielRosenwasser

Ran into this looking into an issue with @athasach:

export type Constructor<T extends object = object> = new (...args: any[]) => T;

export interface Initable {
    init(...args: any[]): void;
}

/**
 * Plain mixin where the superclass must be Initable
 */
export const Serializable = <K extends Constructor<Initable> & Initable>(
    SuperClass: K
) => {
    const LocalMixin = (InnerSuperClass: K) => {
        return class SerializableLocal extends InnerSuperClass {
        }
    };
    let ResultClass = LocalMixin(SuperClass);
    return ResultClass;
};

const AMixin = <K extends Constructor<Initable> & Initable>(SuperClass: K) => {
    let SomeHowOkay = class A extends SuperClass {
    };

    // Error!
    //
    // Type
    //
    //  {
    //      new (...args: any[]): Serializable<K>.SerializableLocal;
    //      prototype: Serializable<any>.SerializableLocal;
    //      init(...args: any[]): void; } & K
    //  } & K
    //  
    // is not a constructor function type.
    let SomeHowNotOkay = class A extends Serializable(SuperClass) {
    };
};

You can see that this is because ResultClass has the type

{
    new (...args: any[]): SerializableLocal;
    prototype: Serializable<any>.SerializableLocal;
    init(...args: any[]): void;
} & K

This is weird, because it has nothing to do with the construct signature. You'd have the same behavior if you swapped this to be {} & K

     let ResultClass = LocalMixin(SuperClass);
-    return ResultClass;
+    return ResultClass as {} & K;
 }

However, if you just leave it as a bare type parameter, TypeScript is okay. The following change will make things work!

     let ResultClass = LocalMixin(SuperClass);
-    return ResultClass;
+    return ResultClass as K;
 }

I guess the question is: is this intentional? Why do we disallow extending from a {} & K but we're okay with extending from K? It feels like we should probably allow this.

Metadata

Metadata

Assignees

Labels

In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions