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

Suggestion: Improve type of constructor on the instance type of a class #32452

Open
4 of 5 tasks
rbuckton opened this issue Jul 17, 2019 · 11 comments
Open
4 of 5 tasks

Suggestion: Improve type of constructor on the instance type of a class #32452

rbuckton opened this issue Jul 17, 2019 · 11 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@rbuckton
Copy link
Member

Search Terms

constructor type

Suggestion

This was previously discussed in this thread: DefinitelyTyped/DefinitelyTyped#36660 (comment)

I could see a possible future mechanism for this.constructor that returned the static side of the containing class without call or construct signatures (but retaining the apparent type of Function).

Currently, the type of the constructor property on most objects is Function. It has been suggested that for any class C {}, the type of the constructor property on the instance should be typeof C. However this suffers a significant drawback in that it severely limits subclasses as any subclass of C must have the same (or compatible) construct signatures as C.

Here is an example of this issue: https://www.typescriptlang.org/play/index.html#code/MYGwhgzhAEDKCuAHApgJwMLitA3gKGmjAC5oIAXVASwDsBzAbgOmAHsaLV5hzXUAKEmUq06ASlzNC5ABZUIAOjDQAvESaEAvnm15QkGAgBGmA9GQAPcshoATQ0jSns+QkdKdRGlu07deAqyI5BCkOEQeIvQANNDuwtT00JoSroRkjoHBimBi3tJyikaq0EEhCkbe2rq01qgAZmDAyHCZzjBphGwclP58pOQAniis9a0oGFgQTDU0dY3NrSZTkuk+PVw8-dBDI2PG7TNAA

Instead, I would suggest a mechanism to type constructor as all of the static members of typeof C but none of the call/construct signatures of typeof C, yet still having an apparent type of Function.

Use Cases

In @ljharb's qs, he'd like to be able to use the constructor property of a Buffer-like object to access the isBuffer method on the constructor in a type-safe way (i.e. obj.constructor.isBuffer).

Examples

/// <reference types="node" />
function isBuffer(obj: { constructor: { isBuffer(obj: any): obj is Buffer; } }) {
  return obj.constructor.isBuffer(obj);
}
const buf = Buffer.alloc(10);
isBuffer(buf); // Buffer class would have a constructor that is `Buffer`

Workaround

There exists a possible workaround for this currently, though it is somewhat complicated:

type StaticMembers<TClass extends Function> = Pick<TClass, keyof TClass> & Function;

class Buffer extends Uint8Array {
  ...
  static isBuffer(obj: any): obj is Buffer;
  ...
}

interface Buffer {
  readonly constructor: StaticMembers<typeof Buffer>;
}

Playground link: https://www.typescriptlang.org/play/index.html#code/MYGwhgzhAEDKCuAHApgJwMLitA3gKGmjAC5oIAXVASwDsBzAbgOmAHsaLV5hzXUAKEmUq06ASlzNC5ABZUIAOjDQAvESaEAvnm15QkGAgBGmA9GQAPcshoATQ0jSns+QkdKdRGlu07deAqyI5BCkOEQeIvQANNDuwtT00JoSroRkjoHBimBi3tJyikaq0EEhCkbe2rrkAJ4ocORg5FTAALLIALZGaBAAPAAqzjCW1nYwAGLwNDxU7AB8JQAKrQDWg8Oxq8i1rABm0ENYEIsAZNBTMy3sTHi01qh7YMDIcJnDkuk+HJT+fKSwJotdpdHqofp1FD7N4oDDHeZMXT3NBPF5vEzHT7pNg-Lg8f6NZqtDrdXp9SHIaHGYYInRAA

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    • Whether this is a breaking change needs to be tested.
  • 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.
@ljharb
Copy link
Contributor

ljharb commented Jul 17, 2019

That sounds great; I'm not concerned with actually constructing or calling the constructor; just about using .constructor to get at static properties.

@fatcerberus
Copy link

fatcerberus commented Jul 17, 2019

this suffers a significant drawback in that it severely limits subclasses as any subclass of C must have the same (or compatible) construct signatures as C

Would it be feasible to simply ignore .constructor when doing a structural type check between known class types? AFAIK .constructor is non-writable and non-configurable so the compiler seems to be in a good position there--we already know a priori that it refers back to the class.

edit: I was being stupid, ignore this.

@ljharb
Copy link
Contributor

ljharb commented Jul 17, 2019

@fatcerberus It's very much writable and configurable, everywhere.

@fatcerberus
Copy link

Hmm, you're right:

> Object.getOwnPropertyDescriptor(C.prototype, 'constructor')
{
  value: [Function: C],
  writable: true,
  enumerable: false,
  configurable: true
}

I could have sworn I remembered it being nonconfigurable. I guess I was thinking of the prototype itself:

> Object.getOwnPropertyDescriptors(C)
{
  length: { value: 0, writable: false, enumerable: false, configurable: true },
  prototype: {
    value: C {},
    writable: false,
    enumerable: false,
    configurable: false
  },
  name: {
    value: 'C',
    writable: false,
    enumerable: false,
    configurable: true
  }
}

My mistake, carry on 👍

@canonic-epicure
Copy link

This will definitely improve the quality of life of those who use static class methods. Currently one have to either cast the constructor property manually, or apply various workarounds as in the example.

What is the point of having the static methods, which you can't (according to the type-checker) call from instances?

@canonic-epicure
Copy link

As always there are lengthy discussions about the simple, quality of life proposal:

for every class:

class SomeClass {
}

define the type of constructor property as typeof SomeClass

interface SomeClass {
    readonly constructor : typeof SomeClass
}

Can we have this simple improvement?

@canonic-epicure
Copy link

This could be easily done with a macros. Then people would start using it right now, finding corner cases, making evaluation. At some point it would be clear if its good change for everyone or its someones, very specific requirement. In the 1st case this "macros" could be included in the TS core, in the 2nd - nobody cares and TS team does not have to care as well.

But TS team does not want to support macroses out of the box to allow language experiments in the user space. Instead they want to proceed through lengthy discussions, which lasts for years.

@Araxeus
Copy link

Araxeus commented Mar 30, 2022

I've encountered problems with accessing static private fields
detailed in #48476

class Test {
    static #staticPrivateField = 'test' ; // (1) #staticPrivateField' is declared but its value is never read.ts(6133)

    constructor() {
        console.log(
            this.constructor.#staticPrivateField // (2) Property '#staticPrivateField' does not exist on type 'Function'.ts(2339)
        )
    }
}

new Test();

note: (1) is a warning that shows up in vscode even if the file is .js, which is pretty annoying

Please fix? 😬

@rbuckton
Copy link
Member Author

Keep in mind this example wouldn't work if you subclass Test, since the subclass constructor will not have that private field. It's much safer to reference Test.#staticPrivateField in this case.

@hasezoey
Copy link

I would also be interested in this so i can generically get the static version without typeof Class everywhere, with for example:

type TypeofClass<T extends { constructor: new (...args: any[]) => any }> = T extends { constructor: infer S } ? S : never;

@mmmmmrob
Copy link

mmmmmrob commented Dec 4, 2023

Another vote for typing obj.constructor for classes to allow access to static members of the class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants