镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Allow type parameters in base class expressions #26542

Description

Search Terms

typescript type parameters in base class expressions

Suggestion

Type parameters in base class expressions.

Use Cases and Examples

Let's start with a simple mixin [playground] (borrowed from proposal #13743):

type Constructor<T = {}> = new (...args: any[]) => T;

class Point {
  constructor(
    public readonly x: number,
    public readonly y: number,
  ) {}
}

// Tagged is a mixin with a type parameter.
const Tagged = <T, B extends Constructor = B>(Base: B) =>
  class extends Base {
    getTag(): T {
      // Just imagine I have a function that returns objects,
      // and I want to case them to T.
      return {} as T
    }
  }

class TaggedPoint extends Tagged(Point) {}

Now if I want to specify the type argument for Tagged, that's easy enough 1 [playground]:

class TaggedPoint extends Tagged<number>(Point) {}

But if I want to pass a derived class's type parameter as that argument, I cannot [playground]:

class UsesTheTag<T> extends Tagged<T>(Point) {
//                                 ^
// error: Base class expressions cannot reference
// class type parameters.
  useTag() {
    const tag: T = this.getTag()
  }
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Footnotes

  1. Note, this requires the default type argument = B shown above, but I have never seen it used in documentation, Stack Overflow, or blogs on the issue of TypeScript mixins. Is that because TypeScript is not actually deducing the type parameter from the function argument? ↩

Activity

  1. mattmccutchen commented on Aug 20, 2018

    @mattmccutchen
    Contributor

    This alternative is working for me (playground):

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    class Point {
      constructor(
        public readonly x: number,
        public readonly y: number,
      ) {}
    }
    
    const Tagged = (Base: Constructor) =>
      class <T> extends Base {
        getTag(): T {
          // Just imagine I have a function that returns objects,
          // and I want to case them to T.
          return {} as T
        }
      };
    
    class UsesTheTag<T> extends Tagged(Point)<T> {
      useTag(): T {
        const tag: T = this.getTag()
        return tag
      }
    }
  2. RyanCavanaugh commented on Aug 20, 2018

    @RyanCavanaugh
    Member

    See #26154 - trying to use an instance type parameter in a base class expression is a conceptual error

  3. mattmccutchen commented on Aug 20, 2018

    @mattmccutchen
    Contributor

    Oops, my code above doesn't actually work because UsesTheTag<T> isn't seen as a subtype of Point. Never mind. This is what I'd like to write, but it gives an error:

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    class Point {
      constructor(
        public readonly x: number,
        public readonly y: number,
      ) {}
    }
    
    const Tagged = <B extends Constructor>(Base: B) =>
      // Error: A mixin class must have a constructor
      // with a single rest parameter of type 'any[]'.
      class <T> extends Base {
        getTag(): T {
          // Just imagine I have a function that returns objects,
          // and I want to case them to T.
          return {} as T
        }
      };
    
    class UsesTheTag<T> extends Tagged(Point) {
      useTag(): T {
        const tag: T = this.getTag()
        console.log(this.x);
        return tag
      }
    }

    Why aren't mixin classes allowed to have type parameters?

  4. thejohnfreeman commented on Aug 20, 2018

    @thejohnfreeman
    Author

    Something else that confuses me, why does this first declaration work but not the second? Why that error at that source location?

    const Tagged = (Base: Constructor) =>
      class <T> extends Base {
        getTag(): T {
          // Just imagine I have a function that returns objects,
          // and I want to case them to T.
          return {} as T
        }
      }
    
    const Tagged = <B extends Constructor> (Base: B) =>
      class <T> extends Base {
    //~~~~~
    // error: A mixin class must have a constructor with a single rest parameter of type 'any[]'.
        getTag(): T {
          // Just imagine I have a function that returns objects,
          // and I want to case them to T.
          return {} as T
        }
      }
  5. mattmccutchen commented on Aug 20, 2018

    @mattmccutchen
    Contributor

    I think I answered my own question: the original superclass could be generic, and any type arguments in the extends clause are passed to the original superclass, not to the mixin. The mixin is not allowed to have type parameters because there would be no way to pass type arguments. Clearly someone thought about this but didn't see fit to write any documentation. ☹️ Hopefully this thread will serve as documentation for the next person to search for the error message.

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    class Point<U> {
      constructor(
        public readonly x: U,
        public readonly y: U,
      ) {}
    }
    
    const Tagged = <B extends Constructor>(Base: B) =>
      class extends Base {
        getTag(): string {
          return {} as string
        }
      };
    
    class UsesTheTag<U> extends Tagged(Point)<U> {
      useTag(): string {
        const tag: string = this.getTag()
        console.log(this.x);
        return tag
      }
    }

    So if we want a derived class that passes a type argument to a mixin, we'll have to go with this approach.

  6. thejohnfreeman commented on Aug 20, 2018

    @thejohnfreeman
    Author

    I think we're getting off track. Here is the fixed version of what you gave earlier, in which I can use the facilities of both Tagged and Point on the final type. The only piece you forgot was to pass Point as the type argument to Constructor (playground).

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    class Point {
      constructor(
        public readonly x: number,
        public readonly y: number,
      ) {}
    }
    
    const Tagged = (Base: Constructor<Point>) =>
      class <T> extends Base {
        getTag(): T {
          // I can use the facilities of the base class
          // (in this case, Point) because I declared that `Base`
          // is a `Constructor` of that class.
          const x: number = this.x
          // Constructions of my class assert the type that I return,
          // so that they get type safety when working with the
          // objects I return.
          return { tag: 'it' } as any as T
        }
      };
    
    class UsesTheTag<T> extends Tagged(Point)<T> {
      useTag(): T {
        // I can use the facilities of the base class (Tagged)
        // and *its* base class (Point).
        const tag: T = this.getTag()
        const x: number = this.x
        return tag
      }
    }
    
    // When I construct an object, I declare the type that its
    // `getTag` and `useTag` methods return.
    const tp = new UsesTheTag<{ tag: string }>(1, 2)
    // When I call `getTag` or `useTag`, TypeScript checks that
    // I use the return value in accordance with the tag type
    // I declared.
    const tag = tp.getTag()
    console.log(tag.tag)
    const sameTag = tp.useTag()
    console.log(tag.tag)
    // `tp` should also be a Point. These should work, just as they
    // did within the implementation of `getTag` and `useTag`.
    const x: number = tp.x
    const y: number = tp.y

    I'm going to consider my original question answered. Thanks for the help! In this case, a type parameter in a base class expression is not what I wanted; I wanted a type parameter in the class expression returned by the mixin.

    There is a follow-up question that I think brushes up against the higher-kinded types limitation that has been documented elsewhere. I haven't seen a well put-together example, though, so I'm going to take this opportunity to try to contribute my own (playground).

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    // The base class.
    class Point {
      constructor (
        public readonly x: number,
        public readonly y: number,
      ) {}
    }
    
    // An extended base class with additional facilities.
    class WeightedPoint extends Point {
      public readonly weight: number = 1
    }
    
    // My mixin just needs the facilities of the base class.
    const SummedSquared = (Base: Constructor<Point>) =>
      class extends Base {
        getSumOfSquares(): number {
          return this.x * this.x + this.y * this.x
        }
      };
    
    // My final subtype expects the extended base class
    // because it needs those additional facilities,
    // but it also wants the mixin applied.
    class SummedSquaredWeightedPoint extends SummedSquared(WeightedPoint) {
      getWeightedSumOfSquares(): number {
        // I want to use the facilities of the base class of
        // the base class (`WeightPointed`), but TypeScript only
        // knows that the base class is a `Point` because that is
        // what the mixin declared.
        return (
          this.x * this.x * this.weight +
          this.y * this.y * this.weight
          //                     ~~~~~~
          // error: Property 'weight' does not exist on type 'SummedSquaredWeightedPoint'.
        )
      }
    }
  7. admosity commented on Oct 12, 2018

    @admosity

    John Freeman (@thejohnfreeman) This playground solves your follow-up question.

    Before:

    const SummedSquared = (Base: Constructor<Point>) =>
      class extends Base {
        getSumOfSquares(): number {
          return this.x * this.x + this.y * this.x
        }
      };

    After:

    const SummedSquared = <T extends Constructor<Point>>(Base: T) =>
      class extends Base {
        getSumOfSquares(): number {
          return this.x * this.x + this.y * this.x
        }
      };

    Pulls the types quite nicely:

    image


    image

  8. aoi-umi commented on Dec 3, 2018

    @aoi-umi

    i have the same question

    import * as mongoose from 'mongoose';
    export declare type InstanceType<T> = T & mongoose.Document;
    export declare type ModelType<T, typeofT> = mongoose.Model<InstanceType<T>> & typeofT;
    
    declare var _Model1: <T>(t?: T) => mongoose.Model<InstanceType<T>>;
    export declare class Model<T, DocOmit={}> extends _Model1() {
        createdAt?: Date;
        updatedAt?: Date;
    }
    
    type FooModelType = ModelType<Foo, typeof Foo>;
    class Foo extends Model<Foo>{
        static async method() {
            //findOne is defined in mongoose Model        
            // interface Model<T extends Document> extends NodeJS.EventEmitter, ModelProperties {    
            //     findOne(conditions?: any,
            //   callback?: (err: any, res: T | null) => void): DocumentQuery<T | null, T>;
            // }
            let rs = await this.findOne();
            //and now the result is mongoose.Document
            
            //but i want to get InstanceType<Foo> 
            let self = this as FooModelType;
            let rs2 = await self.findOne();
            //what i need is 
            //class Model<T, DocOmit={}> extends _Model1<T>() {}
        }
    }

    default
    default

  9. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions