-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Allow specifying interface implements clauses for the static side of classes #33892
Comments
Ref #14600 We discussed #14600 at length and agreed that the syntax interface X {
x: string;
}
interface Y {
y: string;
}
interface Z {
z: string;
}
// OK example
class C implements Y, static X, Z {
static x: string = "ok";
y = "";
z = "";
}
// Error, property 'x' doesn't exist on 'typeof D'
class D implements static X {
} Why not
|
@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 class C implements Y, static X, Z { } Why not this: class C: X, Z implements Y { } 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
as if 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:
And to clarify, I'm talking about syntax only. No functional differences. |
interface P {
x: number;
}
class C: P {
static x: number;
static y: number;
}
C.x; // OK
C.y; // Not OK; only members of P are visible the same way that this works (disregarding excess property checks): interface P {
x: number;
}
const C: P = { x: 0, y: 0 };
C.x; // OK
C.y; // Not OK; only members of P are visible |
I see. To clarify, would class C implements Y, static X, Z { } 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 So if I have an interface class C implements X, static X { } Is that right? |
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 interface Widget {
static create(): Widget;
x: number;
}
class A implements Widget {
static create(): A { ... }
x: number = 1;
}
function factory(clazz: {create(): Widget}): Widget {
const ret = clazz.create();
ret.x = 10;
return ret;
} This is cleaner than the alternative, where I'd have to have separate a WidgetConstructor interface and |
@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: interface Widget {
x: number
}
interface WidgetCtor {
create(): Widget
}
class A implements Widget, static WidgetCtor {
static create(): A { ... }
x: number = 1;
} I personally think that's better. Even in your example, the Also, what if you have an object that is Widget-like? If you do |
You say they're not connected in any way, but you still write both constructor (static) methods and instance methods inside the same As for the static |
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 |
@thw0rted Notice these two lines in your code: interface Widget {
static create(): Widget;
// ~~~~~~~~~~~~~~~~~~
x: number;
}
class A implements Widget {
static create(): A { ... }
x: number = 1;
}
function factory(clazz: {create(): Widget}): Widget {
// ~~~~~~~~~~~~~~~~~~
const ret = clazz.create();
ret.x = 10;
return ret;
} This repetition is exactly what we want to avoid - you defined |
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 |
Ryan, I take your point about duplication. You're saying that having a separate name for the static/constructor interface allows us to reference it easily in the function parameter type. The cost of your solution is adding a new keyword for "implements static" or similar, which introduces the ambiguities discussed in comment 3 above. What if instead we could make a conditional type that took an interface with static properties, and returned an interface with only the static properties except now they're not static? I've never had the knack for the type math needed to make complex transformations in the type space -- I won't bother Dragomir with an at-mention again, but he knows what's up -- but maybe it's possible today. If not, maybe adding type operators to make it possible would be a useful contribution to the language. Imagine:
Bonus points if the magic can turn a class with |
Yes, and it does exactly what I said. When you compile your ES6 class to ES5, the resulting code is pretty much what the browser does with a non-compiled ES6 class. Quoting MDN:
What modern browsers offer is a way to alter the constructor's prototype without actually assigning to it in your code. It's just a syntactic trick.
No, their point is to improve the syntax of working with "one constructor and one prototype."
Yes, but we were talking about a hypothetical let widgetLike = {}
widgetLike.x = 'foo' // should error type '"foo"' is not assignable to type 'number'
let widgetConstructorLike = {}
widgetConstructorLike.create = 42 // should error type '42' is not assignable to type '() => Widget' Could you show how you would implement the types of interface Widget {
static create(): Widget;
x: number;
} How do you tell TS that let widgetLike: Widget = {}
let widgetConstructorLike: Widget = {} ...because those are identical types. Separating the two sides solves this problem: interface Widget {
x: number
}
interface WidgetCtor {
create(): Widget
}
let widgetLike: Widget = {}
widgetLike.x = 'foo' // error
let widgetConstructorLike: WidgetCtor = {}
widgetConstructorLike.create = 42 // error
Actually, it doesn't introduce ambiguities. I simply misinterpreted Ryan's comment. The interface Foo { f: number }
interface Bar { b: string }
// Foo and Bar are forced on the instance
class C implements Foo, Bar {
f = 42
b = 'hello'
}
// Foo and Bar are forced on the constructor
class C implements static Foo, static Bar {
static f = 42
static b = 'hello'
} To me, that's as clear as day. |
OK, I definitely just assumed what "their point" is and have no particular information to back it up. If thinking of JS new-ables as having two distinct pieces is the right mental model, then so be it.
See, to my eye that seems entirely obvious -- of course you can't just call them both That's what led me to the suggestion I made in my second comment this morning, addressed to Ryan, about how it would be nice to have a "magic" |
You should try to avoid that.
Yes, but classes are dual-nature because their sole purpose is to ease the developer in defining the constructor interface and the interface of the object it creates. TypeScript isn't interested in describing classes (as it should), it's interested in describing those two interfaces. This makes sense because classes are just a syntax tool that allows the developer to define everything in one code block, while the constructor and the instance contain the actual logic.
We can just agree to disagree, but the TS developers have to make a choice. Flexibility means handling more use cases, which in turn means TypeScript is more useful. Isn't this more valuable than saving a couple lines of code that never make it to production anyway? |
I came up with two more use cases that also cover generics and inheritance and revealed some more open questions. I was trying to encode some of Haskells monad-instances, to see how typesafe I can get them with TypeScript. Just for reference, here is a simplified version of Haskells monad type-class: class Monad m where
pure :: a -> m a
bind :: m a -> (a -> m b) -> m b Naively, I started with: interface Monad<a> {
pure (x: a) : Monad<a>
bind (f: (x:a) => Monad<b>) : Monad<b>
} The interface Monad<a> {
bind (f: (x:a) => Monad<b>) : Monad<b>
}
interface MonadStatic<a> {
pure (x: a) : Monad<a>
} Next, I tried to implement the Identity-monad. class Identity<a> implements Monad<a> {
private value : a
constructor (value: a) {
this.value = value
}
static pure<a> (x: a) : Monad<a>{
return new Identity(x)
}
bind (f: (x: a) => Identity<b>) : Identity<b> {
return f(this.value)
}
} What's still missing is the static interface instantiation. However, the trick from the handbook does not work here, because const Identity<a>:MonadStatic<a> = class implements Monad<a> { /*...*/ } So here we are. With the syntax proposed above, I think it should be possible to fully instantiate the identity-monad: class Identity<a> implements Monad<a>, static MonadStatic<a> { /*...*/ } However, the signature of Next, I tried to encode the Maybe-monad. The challenge here is that a Maybe offers two constructors. I implemented these as two classes. This made sense because the abstract class Maybe<a> implements static MonadStatic<a> {
static pure<a>(x: a) : Maybe<a> {
return new Just(x)
}
}
class Nothing<a> extends Maybe<a> implements Monad<a> {
constructor () {
super()
}
public bind (f : (x : a) => Maybe<b>) : Nothing<b> {
return new Nothing()
}
}
class Just<a> extends Maybe<a> implements Monad<a> {
private value : a
constructor (value : a) {
super()
this.value = value
}
public bind(f : (x : a) => Maybe<b>) : Maybe<b> {
return f(this.value)
}
} I think the separation of static and dynamic interfaces resolves rather elegant in this example. However, that raises the question if child-class-constructors should inherit the properties of their parent's constructors. In my example, exposing |
worked for me somehow. |
@vzaidman's suggestion seems to be the only working workaround. However, it will only work in scenarios such as this one: the static field does not depend on the type of Having an equivalent of The static field is generic The class MyComponent extends React.Component<Props, State> {
static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): Partial<State> | null { /* ... */ }
} Currently, The static field is mandatory Imagine a server-side rendering architecture in which a component tells the server how its data dependencies should be obtained by using a static method. import { getInitialData, withInitialData } from "another-react-library";
class Page extends React.Component<Props, State> {
static async getInitialData(props: Props) { /* ... */ }
}
export default withInitialData(Page); If we wanted to make the interface SSR<P, D> {
getInitialData(props: P): Promise<D>
}
class UniversalComponent<P, S, D> extends React.Component<P, S> implements static SSR<P, D> { /* ... */ } Such a component would be forced to define what it needs in order to be server-side rendered. class Page extends UniversalComponent<Props, State, Dependencies> {
// Required now!
async static getInitialData(props: Props): Promise<Dependencies> { /* ... */ }
} |
I just realized that my "working" example above leaves you with additional statements in the JS emit. Look at the output on that Playground link, you have "A;" and "B;" at the end. That also means you'll trigger "no standalone statements" linter rules. Maybe we can just make |
It already is legal; see @1EDExg0ffyXfTEqdIUAYNZGnCeajIxMWd2vaQeP 's variant. interface IAStatic {
f(): number;
}
interface IA {
a(): void;
}
const A = class implements IA {
static f(): number {
return 1;
}
a() {}
} satisfies IAStatic; I played around with it. I'm not 100% happy with having two interfaces (one for static, one for instance), but being able to add proper types in a relatively simple manner is already very good. |
Doesn't |
Unfortunately, I'm not an expert on something low-level theoretical like that. My guess would be that one is a class (constructor) and the other one is a reference to a class (constructor). From my experience, they work interchangeably in practice. To be honest, when writing code, I don't care that much about such things... I'm more worried about contracts and APIs being well typed and architected, which is why missing ways to put statics into an binding contract interfaces (as a trivial way to define the contracts) has always been a problem for me. For the fun of it, I put the code into Babel and had it be transpiled to ES5 (in order to strip the // ES6+
const A = class {
static f() {
return 1;
}
};
class B {
static f() {
return 1;
}
}
// ES5
var A = function() {};
A.f = function() { return 1; };
function B() {}
B.f = function() { return 1; };
const A = function() {};
A.f = function() { return 1; };
function B() {}
B.f = function() { return 1; }; Correct me if I'm wrong, though! |
|
I think #32452 is a better solution for this issue. With that implemented this should automatically type checks: interface I {
constructor: { fn(): void; }
}
class C implements I {
static fn(): void { }
} It doesn't require any new syntax, and works as real JavaScript does: Except it's unclear how to extend interfaces with static part, maybe it will need this: interface IA {
constructor: { a: number };
}
interface IB extends IA {
constructor: IA["constructor"] & { b: string };
} |
class A {}
let a: A;
const B = class {};
let b: B; // 'B' refers to a value, but is being used as a type here. Did you mean 'typeof B'
let bWorks: InstanceType<typeof B>; Or you can const B = class {};
type B = InstanceType<typeof B>;
let b: B; I remember there is an issue for allowing any constructor-typed variables to act like a type, but I can't find it now. |
While this workaround does seem to work well for ensuring type safety, the ergonomics aren't great so I would not call it done. Aside from the nonobvious and unusual place of declaration, the after-class-definition requirement means it's impossible to get autocomplete like you do with non-static methods and properties since the type checking is completely disconnected from the declaration. It also means any type errors are (1) dumped onto the same single line, (2) very far away from what actually caused the error, especially if the class is long, and (3) nested at least one level deeper than necessary, since the error message always talks about the class itself failing to type check before mentioning which fields are at fault. Lastly, the intent is obscured, since one would reasonably expect a requirement of the class's statics to be declared along with other requirements of the class -- that is, the In short: yes, it gets the job done, but getting there is unpleasant. |
haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892
haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892
haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892
haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892
haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892
I think going from const A = class implements IA {
static f(): number {
return 1;
}
a() {}
} satisfies IAStatic; to class A implements IA {
static f(): number {
return 1;
}
a() {}
} satisfies IAStatic; 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. |
I think if we have to update the compiler to support a new syntax anyway, |
wip convert Node Crypto to class haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892 wip convert node crypto CBCCipher to class This concludes the conversion, started in f46e972, of the nodejs platform’s crypto.js file to TypeScript. Convert web’s crypto.ts to use classes TODO need to format, but first document what I did
wip convert Node Crypto to class haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892 wip convert node crypto CBCCipher to class This concludes the conversion, started in 18ba2a0, of the nodejs platform’s crypto.js file to TypeScript. Convert web’s crypto.ts to use classes TODO need to format, but first document what I did
wip convert Node Crypto to class haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892 wip convert node crypto CBCCipher to class This concludes the conversion, started in 18ba2a0, of the nodejs platform’s crypto.js file to TypeScript. Convert web’s crypto.ts to use classes TODO need to format, but first document what I did
wip convert Node Crypto to class haven't formatted this — will do once contents of class are correct Use of `satisfies` comes from suggestion in [1]. [1] "Allow specifying interface implements clauses for the static side of classes": microsoft/TypeScript#33892 wip convert node crypto CBCCipher to class This concludes the conversion, started in 18ba2a0, of the nodejs platform’s crypto.js file to TypeScript. Convert web’s crypto.ts to use classes TODO need to format, but first document what I did
It’s hard to tell what’s happened here due to all of the indentation changes. It’s the following: - remove the temporary underscored interfaces introduced in e4c4b9c, and integrate their contents into the class constructor and body - convert prototype function properties to methods (removing the `this` argument) - convert Crypto.CipherParams to a property - add `satisfies` operator for compiler to validate that static methods of Crypto classes implement ICryptoStatic (approach suggested in [1]) [1] microsoft/TypeScript#33892
It’s hard to tell what’s happened here due to all of the indentation changes. It’s the following: - remove the temporary underscored interfaces introduced in b8a8f5d, and integrate their contents into the class constructor and body - convert prototype function properties to methods (removing the `this` argument) - convert Crypto.CipherParams to a property - add `satisfies` operator for compiler to validate that static methods of Crypto classes implement ICryptoStatic (approach suggested in [1]) [1] microsoft/TypeScript#33892
It’s hard to tell what’s happened here due to all of the indentation changes. It’s the following: - remove the temporary underscored interfaces introduced in b8a8f5d, and integrate their contents into the class constructor and body - convert prototype function properties to methods (removing the `this` argument) - convert Crypto.CipherParams to a property - add `satisfies` operator for compiler to validate that static methods of Crypto classes implement ICryptoStatic (approach suggested in [1]) [1] microsoft/TypeScript#33892
It’s hard to tell what’s happened here due to all of the indentation changes. It’s the following: - remove the temporary underscored interfaces introduced in 3f5ec6f, and integrate their contents into the class constructor and body - convert prototype function properties to methods (removing the `this` argument) - convert Crypto.CipherParams to a property - add `satisfies` operator for compiler to validate that static methods of Crypto classes implement ICryptoStatic (approach suggested in [1]) [1] microsoft/TypeScript#33892
This comment was marked as outdated.
This comment was marked as outdated.
Your code has a bug, the correct return type of interface TestDynamic {
dyn(): void;
}
interface TestStatic {
parse(): TestDynamic;
}
const Test = class implements TestDynamic {
constructor() {}
static parse(): InstanceType<typeof Test> {
return new this();
}
dyn() {}
} satisfies TestStatic; |
This issue has been open for about 5 years now, what's the status? |
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:
When I first wanted to do this (before looking at the docs), I tried to do it in this fashion:
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:
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:
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: