tc39 / proposal-type-annotations

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

Suggestion: use decorators to specify types #147

Open brad4d opened 2 years ago

brad4d commented 2 years ago

Carving out space for type information, is really just providing space for metadata. The decorators proposal is already working toward doing that.

Suppose the support for decorators were expanded in these ways:

  1. A built-in @type(args) decorator is added to the language. It is ignored at runtime.
  2. The things you can decorate are expanded to include:
    • function declarations
    • variable declarations
    • parameter declarations
    • object literal properties & methods
    • arbitrary expressions.

You now have a way to decorate anything you like with type information.

For things that TC39 can agree make sense as type information to represent in a type-checker-agnostic way, we could allow passing some standard objects and / or an options bag.

For type-checker-specific information you could just pass a literal string to @type, which the type checker would parse and check.

If we decide some kind of runtime behavior is a good idea, then @type can actually do something instead of being a no-op.

Example:

@type(String) const GREETING = 'Hi';
@type({ returns: String})
function generateGreeting({ @type(String) name }) {
  return `${GREETING}, ${name}.`;
}
ljharb commented 2 years ago

It seems like you'd need to be able to decorate functions, both parameters and arguments, object properties, and variables, before this is viable.

brad4d commented 2 years ago

I've updated the OP with an expanded list of things that would need decoration

theScottyJam commented 2 years ago

It is ignored at runtime. I'm almost wondering if this good be a healthy direction for future type-checkers to take - instead of providing comment-only decorators, a type-system could provide a runtime library with their own decorator definitions that does actual runtime checks when it's performant to do so. This was also an idea that was (very) briefly brought up over here.

Decorators could be combined with template literals, allowing for more expressive syntax, for example:

const myInterface = makeInterface`{
  x: number;
  y: number;
}`

@type`number | { x: ${myInterface} }`
let x = 2

Perhaps a compile-time step could still be used when preparing a production build, to turn these type annotations template literals into some sort of object-literal format, so a type-parser doesn't have to be bundled with the runtime code.

brad4d commented 2 years ago

@theScottyJam That's a very interesting idea!

I like that it provides a very neat way to know the start and end of the type information without requiring either complex grammar rules for the JS engine to implement or imposing limitations on what the user's chosen type system can define for its own syntax.

hax commented 2 years ago

Using decorator for typing is possible, but not as convenient as type annotation.

Note, because @type would look up normal identifier type, and it's very unlikely we could add globalThis.type, so it would become something like @DecoratorNamespace.type(String).

But decorators would be much useful to implement user-land type guard/coerce, because decorators have runtime effect (and cost).

Anyway, the most important point is, it does not match the goal of "make TS/flow/Hegel... just valid JS". So decorator-based solution could coexist with type annotations, but can't replace it.

ljharb commented 2 years ago

Also, if we're using decorators and template literals, then couldn't each type system (TS, Flow, etc) just provide their own decorator, with no changes needed to the language?

Has anyone tried a system like that for, say, TS?

theScottyJam commented 2 years ago

@hax

Anyway, the most important point is, it does not match the goal of "make TS/flow/Hegel... just valid JS". So decorator-based solution could coexist with type annotations, but can't replace it.

It does not match the use case of "make TS/flow/hegel... just valid JS", but from my understanding, supporting the existing syntax of current type-safe languages is more of a secondary goal - it's nice to be able to have backwards support for existing syntax, but not required. The primary goal is to just have a syntax-space within JavaScript to supply type information, and I think this annotation system proves that such a system will exist in the near future. A type-system could use such an annotation system soon, and provide compile-time behaviors to the different annotations. They might even choose to not give the annotations any runtime semantics at all (though, they would be missing out on a great opportunity if they don't supply any runtime semantics to these annotations).

Now, there's a whole set of different pros and cons related to using annotations, and perhaps the authors of this proposal are not willing to accept the cons assosiated with such an idea, but still, it is an alternative that fulfills the primary use case as I understand it.

@ljharb

Also, if we're using decorators and template literals, then couldn't each type system (TS, Flow, etc) just provide their own decorator, with no changes needed to the language?

Correct, I don't feel like we would need a language modification if we were to do something like this. Unless we wanted to try and make a standard, decorator-based type system. But, it would probably be good to see some non-standard type-systems using something like this first before we attempt to make a standard one. And before userland decorator-based type systems can happen, I'm sure we'll need to see some more progress with decorators (especially with follow-on proposals, that provide the ability to decorate stuff besides methods and classes).

matthew-dean commented 1 year ago

I like this idea, because this could also be combined with the metadata proposal to add those types as metadata at runtime.

matthew-dean commented 1 year ago

.

aharbavets commented 1 year ago

I don't like this idea. This will add huge overhead in runtime while it's not needed at runtime at all. Types are most useful at the development time for linting

matthew-dean commented 1 year ago

@gorsash

This will add huge overhead in runtime while it's not needed at runtime at all.

  1. It won't necessarily add overhead in runtime. And certainly not "huge overhead".
  2. The idea it's not needed at runtime is purely a subjective opinion, and also demonstrably false even in TS's own documentation. i.e. TS demonstrates plenty of use cases where you need to check the type at runtime. The difference here is that you could check the exact type vs only some of the supported JS primitives. Furthermore, there are cases where you want to "communicate" the type in polymorphic code, between server and client. It's why tools like Deepkit exist, but Deepkit works only by patching TypeScript to provide the above functionality.

If you don't need this, that's great! Adding this functionality would not force you to make any change in authoring.