Skip to content

Allow for static members to reference class type parameters (Error: TS2302) #32211

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

Closed
4 of 5 tasks
geoyws opened this issue Jul 2, 2019 · 12 comments
Closed
4 of 5 tasks
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@geoyws
Copy link

geoyws commented Jul 2, 2019

Search Terms

  1. [allow for static members to reference class type parameters] (Allow static members to reference class type parameters #24018)
  2. ts2302

Suggestion

Allow for static members to reference class type parameters because it saves typing.

Perhaps we could use a factory to dynamically create classes based on the generic type provided.

Use Cases

import Big from 'big.js';

// db column
class Column<T> {
  static readonly _name: string;
  static readonly comment: string;
  static readonly dataType: typeof T = T; // error: Static members cannot reference class type parameters ts(2302)

  constructor(readonly value: T) {}
}

class Bigint {
  constructor(readonly value: Big = new Big(0)) {}
}

class Amount extends Column<Bigint> {
  static readonly _name: string = '"amt"';
  //static readonly dataType: typeof Bigint = Bigint; // this would be unnecessary
}
														
class Quantity extends Column<BigInt> {
  static readonly _name: string = '"qty"';
  //static readonly dataType: typeof Bigint = Bigint; // this would be unnecessary
}

Examples

See previous section.

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. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@poseidonCore
Copy link

poseidonCore commented Jul 2, 2019

If so, then what type is Column.dataType, given that as a static property, it must exist even without any instance being generated?

This is basically the first question in #24018, and I can't see how you have answered it yet.

@geoyws
Copy link
Author

geoyws commented Jul 2, 2019

The type would be provided by the subclass i.e.:

class Amount extends Column<Bigint> { // <— here the generic type is provided
  static readonly _name: string = '"amt"';
  // (inherited) static readonly dataType: typeof T = T; // hence this would be `typeof Bigint = Bigint`
}

@poseidonCore
Copy link

I realise your intent, but what I am trying to say is that it does not make sense with existing TS patterns.

There is nothing about class Column<T> {...} that says it must be extended for T to be known.

Basically, at the point of consuming Column.dataType, is it any or unknown or (at best) a default class as in Column<T extends Big = Big> {...}?

If your proposal is "Allow for static members to reference class type parameters, defaulting to any | unknown | default type", then that might make sense, but as it is, it is not compatible with current expectations for consuming a static class that has not been extended.

@geoyws
Copy link
Author

geoyws commented Jul 2, 2019

Hold on. Apologies, I was wrong about the use of typeof… I thought it was supposed to fetch me a function constructor but instead it’s TS’ way to get the type of a variable.

How about the below?

import Big from 'big.js';

interface ICtor {
  new (...args: any[]) : {};
}

// db column
class Column<T> { // T doesn’t need defaulting as `{} as T` defaults to `{}` even if T is a non-object primitive
  static readonly _name: string;
  static readonly comment: string;
  static readonly dataType: ICtor = ({} as T).constructor; // error: Static members cannot reference class type parameters ts(2302)

  constructor(readonly value: T) {}
}

class Bigint {
  constructor(readonly value: Big = new Big(0)) {}
}

class Amount extends Column<Bigint> {
  static readonly _name: string = '"amt"';
  //static readonly dataType: ICtor = ({} as Bigint).constructor; // this would be unnecessary
}

class Quantity extends Column<BigInt> {
  static readonly _name: string = '"qty"';
  //static readonly dataType: ICtor = ({} as Bigint).constructor; // this would be unnecessary
}

@superamadeus
Copy link

@geoyws You still seem to be misunderstanding. You cannot use a type to get a constructor for the type. Furthermore, ({} as BigInt).constructor returns the same value as ({}).constructor; the Object constructor. This is because type assertions do not actually create an instance of the type they specify, they're just hints to the compiler as to what type the value is supposed to be.

So you could never take a generic parameter T and automatically get an instance or reference to a constructor of it in the first place.

To solve the case you've described, you could use mixins like so:

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

export interface ColumnConstructor<T> {
  readonly _name: string;
  readonly comment: string;
  readonly dataType: Constructor<T>;

  new (value: T): Column<T>;
}

interface Column<T> {
  readonly value: T;
}

function Column<T>(dataType: Constructor<T>): ColumnConstructor<T> {
  return class Column {
    static readonly _name: string;
    static readonly comment: string;
    static readonly dataType = dataType;

    constructor(readonly value: T) {}
  };
}

class Bigint {
  constructor(readonly value: Big = new Big(0)) {}
}

class Amount extends Column(Bigint) {
  static readonly _name: string = '"amt"';
}

class Quantity extends Column(Bigint) {
  static readonly _name: string = '"qty"';
}

Playground Link

@fatcerberus
Copy link

Yeah, I don't see how this can work. Until an instance of the class (or an instance of a subclass) is constructed, you don't actually have a type T--because it hasn't been passed in yet. Effectively, you're trying to access a variable that's not in scope yet; it'd be like trying to ask for the value of a function parameter outside of the function: the question itself is meaningless.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 2, 2019
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 2, 2019

Every attempt to do this ultimately originates in a conceptual confusion. Critically, the constructor object for a generic class is not generic -- it contains generic construct signatures, which is a completely different thing.

@geoyws
Copy link
Author

geoyws commented Jul 3, 2019

@fatcerberus @RyanCavanaugh thanks for your time guys.

I was thinking perhaps TS could implement this via a class factory that churns out custom constructor functions fitted with the static properties and methods.

@superamadeus appreciate the clarification very much.

@nmain
Copy link

nmain commented Jul 3, 2019

I was thinking perhaps TS could implement this via a class factory that churns out custom constructor functions fitted with the static properties and methods.

You can certainly do something like that for yourself and typescript will typecheck it.

But any idea of typescript automatically inserting a factory like this and then calling it with different values each time you use a generic class with different parameters is type directed emit and out of scope.

@RyanCavanaugh
Copy link
Member

It's also not what ES6 does.

@geoyws
Copy link
Author

geoyws commented Jul 4, 2019

I was thinking perhaps TS could implement this via a class factory that churns out custom constructor functions fitted with the static properties and methods.

You can certainly do something like that for yourself and typescript will typecheck it.

But any idea of typescript automatically inserting a factory like this and then calling it with different values each time you use a generic class with different parameters is type directed emit and out of scope.

It's also not what ES6 does.

So it’s because TS’ design goals prohibit her from doing type directed emit. That explains things. Thanks for the explanation guys.

@miguel-leon
Copy link

This is actually possible to do but with a lot of jumping around.
By inferring the type argument from this (which btw, the compiler also complains about with a different error).

class C_<T> {
    // static myStatic: T;
}

interface Ctor {
    myStatic: this extends typeof C_<infer T> ? T : never;
}

const C: typeof C_ & Ctor = C_ as any;


C.myStatic; // unknown
C<number>.myStatic; // number

playground

Admittedly, it might not make any sense for a static property like this. But it can be very useful for say, a parameter of a static method.

So if it is after all possible, it might as well be allowed with the simpler syntax and save us the hassle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

7 participants