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

Proposal: Make base class expressions able to access class type parameters #36406

Open
5 tasks done
xaviergonz opened this issue Jan 24, 2020 · 5 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@xaviergonz
Copy link

xaviergonz commented Jan 24, 2020

Search Terms

Base class expressions cannot reference class type parameters

Suggestion

Right now it is possible in Ts to generate base classes dynamically (via a function):

function prop<T>(): T {
  // implementation detail doesn't matter
  return null as any;
}

function createDynamicClass<T>(_props: T): { new(): T} {
  // implementation detail doesn't matter
  return null as any;
}

// without generics on the main class all is fine
class C2 extends createDynamicClass({x: prop<number>()}) {
  setX(x: number) {
    this.x = x
  }
}

const c2 = new C2()
c2.x = 20
c2.setX(30)

And it is also possible to reuse a generic when the base class is a "pure" class with a generic type:

class Base<T> { x: number }
class Child<T> extends Base<T> {}

However, currently it is impossible for a function that generates a dynamic class to re-use the type from the child class:

// error: Base class expressions cannot reference class type parameters
class C3<T> extends createDynamicClass({x: prop<T>()}) {
  setX(x: T) {
    this.x = x
  }
}

const c3 = new C3<number>()
c3.x = 20
c3.setX(30)

There's a (somehow ugly) workaround, which is encapsulating the generation of the child class in a function and use its result:

function generateC4<T>() {
  return class extends createDynamicClass({ x: prop<T>() }) {
    setX(x: T) {
      this.x = x
    }
  }
}

const C4 = generateC4<number>()
const c4 = new C4()
c4.x = 20
c4.setX(30)

But it is quite ugly, and results in a new class being generated for each generic when it is not actually needed (just for the typings).

The proposal is basically to lift this constraint and allow it to be used (like it can be easily used for pure classes).

Use Cases

Right now in mobx-keystone it uses such a pattern (a function that generates a dynamic base class) to generate models:

  class Point extends Model({
    x: prop<number>(),
    y: prop<number>(),
  }) {
    @modelAction
    setXY(x: number y: number) {
      this.x = x
      this.y = y
    }
  }

const numberPoint = new Point({x: 10, y: 20})

but in order to support generics, rather than just doing this:

  class Point<T> extends Model({
    x: prop<T>(),
    y: prop<T>(),
  }) {
    @modelAction
    setXY(x: T, y: T) {
      this.x = x
      this.y = y
    }
  }

const numberPoint = new Point<number>({x: 10, y: 20})

users have to resort to a factory pattern like this:

function createPointClass<T>() {
  class Point extends Model({
    x: prop<T>(),
    y: prop<T>(),
  }) {
    @modelAction
    setXY(x: T, y: T) {
      this.x = x
      this.y = y
    }
  }
  return Point
}
const NumberPoint = createPointClass<number>()
const numberPoint = new NumberPoint({x: 10, y: 20})

which is far from ideal (and this is just to get typings right).

Examples

See above

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code

It wouldn't since it is lifting a constraint that currently is just not allowed, so no current code can be using it.

  • This wouldn't change the runtime behavior of existing JavaScript code

The runtime behaviour would be exactly the same, it just addresses a typings problem.

  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@ajafff
Copy link
Contributor

ajafff commented Jan 24, 2020

I only read the first half of the description. However, does this solve your issue?

function createDynamicClass(): { new<T>(): {x: T}} {
  // implementation detail doesn't matter
  return null as any;
}

// without generics on the main class all is fine

class C2<T> extends createDynamicClass()<T> {
  setX(x: T) {
    this.x = x
  }
}

// all good
const c2 = new C2()
c2.x = 20
c2.setX(30)

https://www.typescriptlang.org/play/?ssl=17&ssc=12&pln=1&pc=1#code/GYVwdgxgLglg9mABBATgUwIZTQEQJ5gYC2MEAwgDYYDO1AFAJQBciA3omGgO4A8AKgD5GLVgA8WfAL6S2AKESIA9IsQwiABwpoiaMFCzwkAEzT6YFREbhpqYAORRERLNhTzE6KCBRIwIChY0iBhgeADcspKyssqIXDBQABZwII4A5rpoKKTUiAiISWhOGDBIEFS0wQGqucClaNHlNLlkAEz8AohoothgRrmomNj4hCTkFfQMHXIK1KYAGnTiiHwMMwoFiTDUAHSiiAC8iKLuUVExKhjVaXBwRrIQCNSOEK2HHNyIbYwPrXvvrQADL8dnMoIsAMyAhhAA

@xaviergonz
Copy link
Author

Hi, thanks for looking at it! Sadly it doesn't. I just changed the theoretical examples so they better match the use cases.
The real case relies on type inference for the dynamic class function to work.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Feb 21, 2020
@sisp
Copy link

sisp commented Apr 29, 2021

It would be very helpful if TypeScript supported this feature. Is there any interest and/or progress in implementing it? Any open questions?

@zxh19890103
Copy link

I am facing this problem too.

@eddow
Copy link

eddow commented Oct 15, 2024

Is it still up to debate ?
I'm facing it in the simple legacy case class MyClass<T> extends MyParent<T> {...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants