tc39 / proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
https://tc39.es/proposal-type-annotations/
4.25k stars 46 forks source link

Abstract classes and methods #65

Open justinfagnani opened 2 years ago

justinfagnani commented 2 years ago

The readme talks about the abstract keyword as being up for debate, but there's a bit more to abstract methods than just the keyword. It'd be good to know how abstract methods map to this proposal, both with and without the abstract keyword.

If we get the abstract modifier, does that mean that we also need a body-less method declaration? Are abstract methods realized on the prototype or not?

And since an abstract class is basically a combination of a class and an interface, they might be an argument for keeping interfaces similar to class declarations as in https://github.com/giltayar/proposal-types-as-comments/issues/64.

If we don't get the abstract modifier, I'm curious how TypeScript and others would represent them. There doesn't yet seem to be syntax space for annotating the class itself (not members) in any way.

giltayar commented 2 years ago

The proposal does not discuss what the abstract keyword means. In all the spaces where types are allowed, they are treated as comments. So if abstract is allowed as a "type space", it's semantic meaning would not be covered by this proposal.

ljharb commented 2 years ago

I’m not sure why abstract is included at all - there’s no such concept in JavaScript, so why would we need types for it?

justinfagnani commented 2 years ago

Because existing type systems let you declare them, creating a mix of implementation and interface. Without the keyword, what's the intended path to allow type systems to support abstract classes?

edit: even an example would help. How would a type system allow annotating a class to declare that it's abstract?

wparad commented 2 years ago

Why does it matter, there are lots of things we can do to extend on the type system, the question should be what must we do as part of this proposal. Abstracts are not a requirement, and while may be valuable, as such should not be part of the proposal.

justinfagnani commented 2 years ago

If the goal is to be able to migrate from proprietary type syntaxes to this one, but continue to use those type-checkers, then this proposal absolutely does need to consider how to map existing constructs into it. This proposal does not exist in a vacuum free from existing typed JS variants.

wparad commented 2 years ago

those type checkers can also make adjustments to support the new version of js that has types. No different than any other new feature in JS. We don't need to consider and should definitely NOT consider, supporting previous failures, when deciding how to proceed. Doing so just makes the outcome shit. garbage in garbage out.

This proposal does not exist in a vacuum free from existing typed JS variants.

It actually does, and we should consider first what we want it to be, and then only consider what is the impact to other tools, and then whether or not we care. Tomorrow, everyone using TS today may decide to dump it, and then all considerations for TS would have been a waste.

  1. First what we want
  2. Then the impact
  3. Then how we want to deal with that.

Caring about the impact to TS (and others) first is just irresponsible to the js community.

ljharb commented 2 years ago

I think that having abstract class types in TS is also a category error - it's not a concept that exists at runtime, and pretending at compile-time that the concept exists is at best, a potential source of bugs. I would be strongly against the abstract keyword being an explicit part of this proposal (of course, that's a stage 2 concern, not a stage 1 concern).

justinfagnani commented 2 years ago

I would strongly disagree, I think that's factually incorrect.

An abstract class in TypeScript very much does exist at runtime. It creates a real class with everything implemented in the abstract classes: constructor, fields, methods, static initializers. In fact, all TypeScript does is remove the abstract keyword and type annotations: https://www.typescriptlang.org/play?#code/IYIwzgLgTsDGEAJYBthjAgYge2wg3gFAIIAeCAvAgIwDchxCIwUAFAJQFLYB2Y2yAKYA6ZNgDmrAOTMoU9rQQBfQkqA

The point of an abstract class is to provide a partial implementation of a class that only work properly when properly subclassed. That's a concept that exists outside of the type system

wparad commented 2 years ago

If abstracts exist at runtime in TypeScript, then there is a major fuck-up there, and we should definitely not port that to JS. Abstracts are by definition abstract, they should never be available nor need to be available at runtime.

justinfagnani commented 2 years ago

Abstract classes serve as base classes, they need to exist at runtime.

ljharb commented 2 years ago

@justinfagnani you're totally correct that the class exists at runtime; but the abstractness doesn't, which is the problem. At runtime, the class is a NON-abstract class - the opposite (inverse? not sure what's the right term here) of the thing it's declared to be.

hax commented 2 years ago

I think the best way might be adding "abstract" to JS with runtime semantics as:

abstract class Foo {} work as

class Foo {
  constructor() {
    if (new.target === Foo) throw new TypeError()
  }
}

abstract method() work as method() { throw new TypeError() }.

wparad commented 2 years ago

I think the best way might be adding "abstract" to JS with runtime semantics as

That's making the assumption we need "abstract", do we actually need it?

ljharb commented 2 years ago

Exactly. If we needed it, actually adding it would be the proper approach - then adding types for it wouldn't be a category error. I'm pretty convinced we don't need it.

matthewp commented 2 years ago

I'm not arguing in favor of abstract but the fact that it doesn't work as expected at runtime is not a demerit any more than the rest of this proposal that also doesn't work as expected at runtime, ie const add = (s: number) => accepts a string at runtime.

ljharb commented 2 years ago

It’s that it doesn’t represent a concept that exists at runtime; number does.

Obviously this proposal has a lot of free form space where people can put whatever they want, including “abstract”; it just doesn’t need an explicit carve out.

justinfagnani commented 2 years ago

@giltayar

The proposal does not discuss what the abstract keyword means. In all the spaces where types are allowed, they are treated as comments. So if abstract is allowed as a "type space", it's semantic meaning would not be covered by this proposal.

I'm not talking about the semantic meaning, but the syntax space for abstract members which any language that has abstract classes allows.

abstract class Foo {

  // non-abstract method, goes on prototype
  a() { return 1; }

  //abstract method, without method body.
  abstract b(): string;
}

Would an abstract method be an entirely type-only member, not added to the prototype?

matthewp commented 2 years ago

@ljharb

I don't think that's the bar here, interfaces (duck typing) are not a concept at runtime either but those can be used as type annotations too, not just primitive types.

I would say that a general criticism of this proposal is that the whole thing feels like a hack around the standards process whereby a feature doesn't have to be argued on its merits due to it being "just a comment".

ljharb commented 2 years ago

@matthewp interfaces are absolutely a runtime concept - “thenable”, or “toStringable”, for example - everything first-class protocols captures.

matthewp commented 2 years ago

You're referring to specific interfaces and not a generic "object has this shape" interface which is what the TS annotations allow.

wparad commented 2 years ago

To make sure everyone is on the same page, what does runtime generic interface usage look like to you, what are the implications and usages of that?

ljharb commented 2 years ago

@matthewp Yes, there is no first-class "interface" mechanism - that's what the first-class protocols proposal would add - but the concept exists and is used in a LOT of builtin methods, as well as the ecosystem at large, unlike that of abstract classes, which only exists in JS-adjacent type systems.

@wparad .valueOf, .toString, .toJSON, .then (thenable), .length (arraylike), [Symbol.iterator] (iterable), are some pretty critical ones in the language; i'm sure i could add to the list if i thought about it for more than a few seconds.

wparad commented 2 years ago

Sorry, I was asking @matthewp to clarify the confusion from his end, because I can see the perspective from both sides.

giltayar commented 2 years ago

@justinfagnani

Would an abstract method be an entirely type-only member, not added to the prototype?

As the proposal now stands, abstract class Foo {} would be the same as /* abstract */ class Foo {}. Same for private, public. These are type "traits" that are defined on the class (or property or method).

There is a suggestion in the proposal to add a sigil like % to these "traits" to enable type systems to define whatever traits they like, e.g. %const or %uplifting.

matthewp commented 2 years ago

@wparad I'm not sure this is a fruitful tangent. I don't think that the interfaces that exist in JavaScript are like the interfaces that are supported by TS. But, I agree with @ljharb in that I think if abstract is a good idea or not should be debated on its own, outside of the context of type annotations.

justinfagnani commented 2 years ago

@giltayar that works for the modifer on the class. I'm asking about the behavior of the modifier on the class members.

abstract is not just a visibility modifier on class members. It allows the member to be bodyless and doesn't add anything to the instance or prototype for that member. This could either be viewed as runtime behavior (if you view it as eliding a member), or that the whole member is a type-only declaration.

Right now this TypeScript:

abstract class Foo {
  a() { return 1; }
  abstract b(): string;
}

produces this JavaScript:

class Foo {
  a() { return 1; }
}

So what I'm asking is, regardless of whether abstract is a keyword or some kind of sigil-delineated modifier, or not included at all, what's the equivalent input would be that when stripped of types produces the same output, and type-checks in TypeScript the same way.

I could see arguments for:

%abstract class Foo implements FooAbstractMethods { a() { return 1; } }


What think will _not_ work is treating `abstract` generically like other visibility modifiers:

```ts
%abstract class Foo {
  a() { return 1; }
  %abstract b(): string;
}

Because 1) there'd be a syntax error since b() is missing a body, and 2) if that syntax were allowed, we don't want b on the prototype.

giltayar commented 2 years ago

@justinfagnani Ah, I missed that. You're right. abstract on a method is different. That is an excellent question!

Given what I know of the different stages in TC39, this will be hashed out in the committee somewhere around stage 2. But I'm interested: what would you like to happen?

If we get the abstract modifier, does that mean that we also need a body-less method declaration? Are abstract methods realized on the prototype or not?

There is another place with bodyless declarations, and that is function overloads. But that is also up for debate, for earily similar reasons.

hax commented 2 years ago

Actually web platform APIs are based on abstract classes. Node and CharacterData are explicitly defined as abstract classes. Another obvious example is HTMLMediaElement. You can never have a direct instance of HTMLMediaElement but the instance of HTMLVideoElement or HTMLAudioElement which are derived classes of HTMLMediaElement. There is also AbstractRange which tell you it's an abstract class from the name.

Note, as the spec all of them are "abstract interface" instead of "abstract class", but it's just because web spec never have the concept of "classes" for host APIs. Web spec use WebIDL to define "interface"s. From JS side, they normally map to constructors, aka. classes. Interface of TS (and other OO languages) is much closer to WebIDL's "interface mixin" and "partial interface".

Node.js also have abstract classes. Before Node v5.5.0, stream Readable, Writable and Duplex are all abstract classes. Developers need to extend them and implement abstract methods _read, _write. (After v5.5.0, stream constructors could accept read, write options to create instance directly. )

JS builtins do not have abstract classes up to now, but consider readonly collections proposal, Map, FixedMap, and ReadOnlyMap are behavioral subtypes of a hypothetical AbstractMap. We may not able to add AbstractMap to JS because adding new level of prototype to Map theoretically could cause web compatibility issue. But I think it's clear that abstract classes have real use cases.

hax commented 2 years ago

If we agree that abstract classes have use cases and could be a JS feature, and assume we adopt the syntax from TS (and most other OO programming languages), the left problem is just how the runtime semantic should be. We at least have three options:

  1. No runtime semantic (as this proposal)
  2. Light runtime semantic (as https://github.com/tc39/proposal-type-annotations/issues/65#issuecomment-1064378442) which only ensure:
    • new AbstractClass() throw TypeError
    • o.abstractMethod() and o.abstractAccessor throw TypeError
  3. Heavy runtime semantic close to first-class protocol proposal which ensure:
    • Non-abstract subclasses have to implement all abstract methods/accessors

Personally I prefer option 2, I'd like to write a separate "abstract modifier" proposal if people here like this direction.