Repository navigation
Description
Search Terms
class static side syntax interface type expression
Suggestion
Currently, you can only specify the static side interface of a class with a declaration. From the handbook:
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}When I first wanted to do this (before looking at the docs), I tried to do it in this fashion:
class Clock: ClockConstructor implements ClockInterface {
...
}And I was surprised to see that it didn't work. My proposal is to make this a valid syntax as it's more intuitive and understandable.
I believe that forcing class expressions conflicts with TypeScript's design goals:
- Produce a language that is composable and easy to reason about.
Why use a class expression when there is no need for it? Why change your actual JavaScript logic for something that exists only in TypeScript and not in your production code.
Use Cases
Anywhere you need to set the interface of the static side of a class without having a need to specify it as an expression.
Examples
Take the example from the playground:
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick();
}
class Clock: ClockConstructor implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}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.
Activity
RyanCavanaugh commentedon Oct 15, 2019
Ref #14600
We discussed #14600 at length and agreed that the syntax
static Tin theimplementslist of a class would be a reasonable place to allow this:Why not
staticmembers in interfaces?#14600 proposed the syntax
This is problematic for a couple reasons.
First, it seems to create a totally meaningless thing:
Second, there would be no way to access a static member through an interface type that declared it!
You would need to have some auxiliary keyword to turn an interface into the static version of itself:
Simply moving the
staticlogic to the class syntax makes this substantially simpler, and (critically) allows classes to implement types that weren't written using the hypotheticalinterface I { static property: T }syntax.Why
implements static T, notstatic implements T?In the presumably-usual case of one instance and one static heritage member, this is substantially cleaner:
vs
[-]Syntax for specifying class static side interface without an expression[/-][+]Allow specifying interface implements clauses for the static side of classes[/+]yandodov commentedon Oct 16, 2019
Ryan Cavanaugh (@RyanCavanaugh) I understand your points and agree with all of them. But I don't understand what's the issue with my proposed syntax without the
statickeyword. I mean, instead of this:Why not this:
Your syntax could be read the correct way like this:
but due to the commas, it could also be read like this:
which would make
Zappear out of place. It could even be read like this:as if
CimplementsY,Zand a staticXwhich makes no sense. I think the issue here is that the comma is used as both a separator for symbols and keywords.My proposal has a clear separation of the static and instance sides:
Yours doesn't:
I just think my proposal leaves less ambiguity.
Some other benefits:
statickeyword. It was designed for usage inside the class body and if you avoid using it, you'll avoid assigning another TypeScript-only meaning to itXandZare visually closer toC, which makes more sense since you'll be usingC.memberOfXandC.memberOfZlet foo: stringandconst bar: number. Addingclass C: X {}simply follows that patternAnd to clarify, I'm talking about syntax only. No functional differences.
RyanCavanaugh commentedon Oct 16, 2019
:in TypeScript always means "is of type", butimplementshas a different meaning ("is assignable to"). IOW the implied semantics of:arethe same way that this works (disregarding excess property checks):
yandodov commentedon Oct 17, 2019
I see. To clarify, would
staticact as a modifier? If I have:I should read it as "C implements interface Y, interface X as static, and interface Z" instead of "C implements interface Y and interfaces X and Z as static," right?
When I initially read your comment, I thought
staticwould act likeimplementsand you list items after it. Instead, it would act as a modifier to the items ofimplements?So if I have an interface
Xthat must be type-checked in both the instance side and the static side, I would do:Is that right?
thw0rted commentedon Oct 17, 2019
I don't really follow Ryan's goals in his second post here.
If you made an class with only static methods then you couldn't do much with instances of it either.
I don't think that's the point. The reason I want to see
staticin an interface is to avoid needing to split a mix-in definition:This is cleaner than the alternative, where I'd have to have separate a WidgetConstructor interface and
static implementit. That feels clunky and leads to the potential confusing cases @hdodov lists a few posts back, where there isn't really a "right" way to visually parse the sequence of keywords.yandodov commentedon Oct 17, 2019
James Bromwell (@thw0rted) the constructor and the instance are two completely separate entities. In JavaScript, the API of one is not connected in any way to the API of the other. Because they have different APIs, they probably function differently as well—you can't use a constructor as an instance and vice-versa. Therefore it makes sense to define their types with two separate interfaces.
Your example could be rewritten as:
I personally think that's better.
Even in your example, the
Widgetinterface has a methodcreate()that should return aWidgetinterface... that should also havecreate()and return aWidgetwithcreate()and so forth? Separating those types in two interfaces solves that problem.Also, what if you have an object that is Widget-like? If you do
let obj: Widgetit becomes really confusing that this object uses an interface that has a staticcreate()method. By adding a static member to an interface, you imply that it has a static and an instance side and can therefore be used in classes only. Why limit yourself like that? By specifying two interfaces, you avoid that problem too.thw0rted commentedon Oct 17, 2019
You say they're not connected in any way, but you still write both constructor (static) methods and instance methods inside the same
class { }block, right? That's what I'm saying in my previous comment. There's a certain parallelism in the single-interface version, one interface that describes (a portion of) one class, that's lacking in the two-interface version you describe.As for the static
create()method oflet obj: Widget, can't I callobj.constructor.create()if I want to? I'm not an expert in how ES6 classes differ from constructor functions with prototypal inheritance, but I can say that this at least works in Chrome and Node, whether or not it's "correct".yandodov commentedon Oct 17, 2019
Yes, because in terms of JavaScript, you have no reason to separate them. When a class extends another class, it extends both the instance and the static side. Don't forget that classes are just syntactic sugar. In the end, all static members appear on the class (constructor) itself, while the instance methods appear in its prototype. So they are separated in JavaScript as well.
Yes, but what value does that give you? You can't describe one class. You describe one constructor and one prototype. You describe two things at once which, in the realm of types, complicates things. Logically, they describe "one class" but they serve a different purpose. One might even say, they're different types. And types is what TypeScript is interested in.
Yes, you can. But that's different from
obj.create(). A classXwith staticcreate()would have its method called withX.create(). Saying an object has the same interface would imply that it should haveobj.create(), right? But (back to your example) if static properties are assigned toobj, where should the instance memberxbe expected?obj.x? Why are suddenly static and instance referring to the same thing? Because we united them in an interface and we shouldn't.RyanCavanaugh commentedon Oct 17, 2019
James Bromwell (@thw0rted) Notice these two lines in your code:
This repetition is exactly what we want to avoid - you defined
static createininterface Widget, and have no way to reference that declaration to define the shape ofclazz.thw0rted commentedon Oct 18, 2019
ES6 already has first-class support in modern runtimes.
It might just be syntactic sugar once you're down in the guts of the engine, but all the seams have been fully plastered over when viewed from the outside. The whole point of ES6 classes is to stop the dichotomy of "one constructor and one prototype".
I don't follow your last paragraph about "where instance member
xis expected". An instance of a class that implements an interface with static properties would not have those properties, because that's not whatstaticmeans. Maybe a more concrete example would clarify your concerns?47 remaining items
jthoward64 commentedon May 14, 2023
I think going from
to
would probably address most/all of the concerns in the issue while hopefully not adding much compiler overhead and making the information available to autocomplete. I haven't looked much into the implementation here, but it doesn't seem like that big of an adaptation, especially considering the improved ergonomics it allows.
thw0rted commentedon May 15, 2023
I think if we have to update the compiler to support a new syntax anyway,
class A extends B implements IA satisfies IAStatic {...}is more ergonomic thanclass A extends B implements IA { ... } satisfies IAStatic. Putting one of the three relationships after the declaration, likely several pages of code later, was already going to be worse. The only advantage is that it can be used as a workaround right now....Convert crypto.ts to use classes
Convert crypto.ts to use classes
Convert crypto.ts to use classes
Convert crypto.ts to use classes
Convert platform crypto.ts files to use classes
Convert platform crypto.ts files to use classes
Convert platform crypto.ts files to use classes
Convert platform crypto.ts files to use classes
gylove1994 commentedon Dec 1, 2023
gBasil commentedon Jan 14, 2024
There still seems to be a shortcoming is the current suggested workaround:
Would this be fixed by the syntax change (not requiring a variable) and is there a way to work around this for now, or am I making some sort of mistake and this isn't an issue?
Specifically, I ran into this issue while trying to solve the bigger issue of the type of
Testbeingtypeof (Anonymous class). That means that this code:along with removing the
: typeof Testfrom the static method, gives this error:The following is a workaround, but it doesn't seem very clean and it would be nicer to not have to do this:
The type of
Testis stilltypeof (Anonymous class), however.ExE-Boss commentedon Jun 26, 2024
Basil (@gBasil)
Your code has a bug, the correct return type of
Test.parse()isInstanceType<typeof Test>:Sebastian-Nielsen commentedon Jul 6, 2024
This issue has been open for about 5 years now, what's the status?
eczn commentedon Dec 24, 2024
it could be rewritten with
satisfieskeyword before class body: (a new feature)