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

Implement the updated JS decorators proposal #48885

Closed
1 of 5 tasks
arackaf opened this issue Apr 29, 2022 · 34 comments · Fixed by #50820
Closed
1 of 5 tasks

Implement the updated JS decorators proposal #48885

arackaf opened this issue Apr 29, 2022 · 34 comments · Fixed by #50820
Assignees
Labels
Committed The team has roadmapped this issue Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@arackaf
Copy link

arackaf commented Apr 29, 2022

Suggestion

Implement Decorators!

🔍 Search Terms

TypeScript Decorators

List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.

Decorators

✅ Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Not sure - this would be badly breaking against your legacy decorators feature, but I think it meets the goal to align TS with JS

⭐ Suggestion

The TypeScript proposal is now Stage 3!!!

https://github.com/tc39/proposal-decorators

We've all seen the issue from hell requesting better typing support for decorators

#4881

but now that the proposal is moving forward, currently at Stage 3, I thought I'd open this issue to see if there are plans to finally, fully implement (and type) the ES version of decorators.

📃 Motivating Example

N/A - just implementing new JS features, to keep JS and TS aligned.

💻 Use Cases

All the various ways decorators can be used.

@DanielRosenwasser DanielRosenwasser added this to the TypeScript 4.8.0 milestone Apr 29, 2022
@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Apr 29, 2022
@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Committed The team has roadmapped this issue labels Apr 29, 2022
@DanielRosenwasser
Copy link
Member

I think the first step will be implementing decorators. We're not going to try to model the "decorators can change the types of things" portion at first, but I think we can get something good and working.

@arackaf
Copy link
Author

arackaf commented Apr 30, 2022

@DanielRosenwasser that sounds perfect.

But just to be clear, annotating how a decorator change’s a type will be on the roadmap long term?

@glen-84
Copy link

glen-84 commented May 1, 2022

Shhh, don't say "long term". 😉

@wycats
Copy link

wycats commented May 13, 2022

@DanielRosenwasser the biggest "type-related" transformation that comes up for me in the current implementation of decorators is communicating that a field decorator initializes the field's value.

I think that this would just fall out of accessor decorators (since the decorator turns into a getter, the concept of an uninitialized value no longer makes sense). Is that right?

Also, a regular field decorator might still want to communicate that the value is initialized (for example, a decorator that implements a default by returning a new initializer). Is it possible to address the issue of communicating that a decorator initializes the value on a different timeframe than arbitrary type transformation, or is it just as hard?

@Jamesernator
Copy link

Jamesernator commented May 14, 2022

We're not going to try to model the "decorators can change the types of things" portion at first, but I think we can get something good and working.

Simple transform decorators should be easy enough to support right?

Like:

function wrapInBox<R>(f: () => R): () => { value: R } {
    return () => { value: f() };
}

class Foo {
    @wrapInBox
    method(): number {
        return 3;
    }
}

const foo = new Foo();
foo.method(); // should be { value: number }, same as regular function wrapping

Has very little difference to (type inference wise):

class Foo {
    method = wrapInBox((): number => {
        return 3;
    });
}

(Ignoring the prototype vs own property distinction here).

Isn't it primarily the metadata-based type-mutations that are hard to actually support? (given they need to communicate types across calls)

@trusktr
Copy link
Contributor

trusktr commented Aug 17, 2022

@DanielRosenwasser that sounds perfect.

But just to be clear, annotating how a decorator change’s a type will be on the roadmap long term?

Besides this, also picking types from a class by specific decorator would be wonderful... in the longer term. :D

@trusktr
Copy link
Contributor

trusktr commented Aug 18, 2022

Also, a regular field decorator might still want to communicate that the value is initialized (for example, a decorator that implements a default by returning a new initializer). Is it possible to address the issue of communicating that a decorator initializes the value on a different timeframe than arbitrary type transformation, or is it just as hard?

Do you mean something like this?

class Foo {
  @alwaysString foo = 123 // initializes it to "123"
}

I think TypeScript would create the type right then and there, at class definition. The user cannot (write any code that will)
observe a moment when the value is a number instead of a string, apart from the decorators themselves.

Another thing could be that maybe decorators can augment the type of a class, but not necessarily the type of the thing they decorate directly. For example, this

class Foo {
  @withDouble count = 123
}

could add a new doubleCount variable that is always the double of count whenever count changes. The type would be { count: number, doubleCount: number } where count was not modified.

However, I think that with this new decorator API, decorator functions can be completely generic, with their input and output types completely known by TypeScript. So, this should be possible:

function alwaysString<T, C extends ...>(_: T, context: C): (initial: T) => string {
  if (context.kind !== 'field') return // type narrowing based on union of string values for `kind`

  return initial => initial.toString()
}

and based on something like this, I imagine it totally possible for TypeScript to look at the return type and determine that the final type of the property should always be a string (or to always have any setter, string getter).

But a version with class types not modifiable would still be totally usable!

@ruojianll
Copy link

ruojianll commented Aug 19, 2022

I suggest implement decorators first, then add type support. The current legacy class decorator could return a non-class constructor value, and it won't be recognized by ts type system, but it is still easy to use.

Or we just use legacy version until stage 4 published, but maybe they are waiting us to do something to verify their design in stage 3.

@wycats
Copy link

wycats commented Aug 25, 2022

I suggest implement decorators first, then add type support

As long as @dec accessor foo understands that you don't need to explicitly initialize foo, I think I can live with that.

@wycats
Copy link

wycats commented Aug 25, 2022

I wonder if the narrow case of initializer mapping might be more doable than other kinds of changes caused by decorators.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Aug 25, 2022

This is kind of what I've been thinking recently. I can imagine a world where

  • decorators can return a more-derived type than the initializer of a field
  • the type returned by the decorator needs to be assignable to the annotated type of the field - though the initializer doesn't necessarily need to be

It's enough to help with auto-initialization (which is just removing undefined from a type) and creating more-specialized types (which is turning something like [1, 2, 3] into a WatchedArray<number>/ReactiveArray<number> instead of an Array<number).

The reason for the latter bullet is being able to support something along the lines of #47947, along with some other precedent we have (e.g. async functions need to say they return a Promise<ThingBeingReturned> rather than ThingBeingReturned, and the wishful thinking some of us have in the behavior of --exactOptionalProperties).

@trusktr
Copy link
Contributor

trusktr commented Aug 26, 2022

decorators can return a more-derived type than the initializer of a field

This part works:

class Foo {
  @reactive array = [1, 2, 3] // converts to ReactiveArray<number> for example
}

But what about this?

const f = new Foo

f.array = [4, 5, 6] // Should also accept a plain array and convert to a (or map to the) `ReactiveArray<number>`
f.array // `ReactiveArray<number>`

@trusktr
Copy link
Contributor

trusktr commented Aug 26, 2022

Also what about the case of a totally different but valid type? The following is based on real-world examples, namely for attribute processing with custom elements:

import {numberArray} from 'some-custom-element-lib'

class MyEl extends HTMLElement {
  // also accepts strings (f.e. from HTML attributes)
  @numberArray coords = "1 2 3" // converts to ReactiveArray<number> for example
}

customElements.define('my-el', MyEl)

const el = new MyEL

el.array = [4, 5, 6] // Should also accept a plain array and convert to a (or map to the) `ReactiveArray<number>`
el.array = "7 8 9" // Should also accept a string with space-separate list of numbers and convert to a (or map to the) `ReactiveArray<number>`
el.array // `ReactiveArray<number>`

@trusktr
Copy link
Contributor

trusktr commented Aug 26, 2022

Hmm, now that I think about it, maybe an accessor is all that is needed, because the decorator would then receive an object with get and set functions. The decorator could define each one of those to have separate types, just like class getters/setters currently can.

Example:

// some-custom-element-lib
export function numberAttribute() {
  return {
      get(): ReactiveArrayTriplet<number> {
        // ...
      },

      set(val: `${number} ${number} ${number}` | [number, number, number] | ReactiveArrayTriplet<number>) {
        // ...
      },
  }
}
import {numberArray} from 'some-custom-element-lib'

class MyEl extends HTMLElement {
  // also accepts strings (f.e. from HTML attributes)
  @numberArray accessor coords = "1 2 3" // converts to ReactiveArray<number> for example
}

In this case, the type of the implied getter/setter can be determined from the decorator return value.

There's actually a way to write the above without using the accessor keyword in plain JavaScript, like so:

import {element, numberArray} from 'some-custom-element-lib'

@element // this added decorator is needed for creating reactive accessors in a returned subclass based on decorated fields
class MyEl extends HTMLElement {
  @numberArray coords = "1 2 3"
}

but then I don't see how TypeScript would be able to determine differing get/set types for coords.

@ruojianll
Copy link

ruojianll commented Aug 28, 2022

@trusktr Maybe that should be write in grouped accessors.

class Foo {
accessor coords{
  get():ReactiveArrayTriplet<number>{
    //...
  };
  set(data:`${number} ${number} ${number}` | [number, number, number] | ReactiveArrayTriplet<number>){
    //...
  };
}
}

@ruojianll
Copy link

@wycats In my opinion, the accessor type should be declared in the class. Decorators should be compatible with accessor type.

class Foo{
  @Dec accessor bar?:string //not allowed, bar must be instialized
  @Dec accessor bar2:string|undefined//allowed
}

Decorators shouldn't break the type defined in class. That's also a typescript way to implement.

@uasan
Copy link

uasan commented Sep 5, 2022

Is there any information about TS plans to implement decorator extensions?
https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md

Many real use cases require exactly the extended capabilities of decorators.
Thanks.

@justinfagnani
Copy link

@uasan I'm sure TypeScript wouldn't implement them before they're standardized, so it's really a question for TC39 and the decorators proposal champions.

@trusktr
Copy link
Contributor

trusktr commented Sep 14, 2022

@trusktr Maybe that should be write in grouped accessors.

That defeats the purpose of having decorators: to write concise code and not repeat that pattern on every property. We are here in this thread to use decorators!

@trusktr
Copy link
Contributor

trusktr commented Sep 14, 2022

For the time being, it would be great to have an option to not compile decorators (just like we can leave JSX untouched), so that we can pass output code along to other tools like Babel. This would be a lot simpler to implement, and still useful even after TypeScript gets updated decorators.

@trusktr
Copy link
Contributor

trusktr commented Sep 15, 2022

Along with a decorators: "preserve" or similar option, export needs to be allowed before @decorator. Then we're fully off to the races without TS needing full support yet!

@trusktr
Copy link
Contributor

trusktr commented Sep 15, 2022

Currently we can make this work using @babel/typescript (type check code with tsc separately) by splitting exports from class definitions to avoid errors that tsc currently gives.

This does not work:

export // export is required before stage-3 decorators, currently an error in TS
@stage3Decorator
class Foo {}

This works:

@stage3Decorator
class Foo {}
export {Foo}

Your babel config plugins will look something like this:

	plugins: [
		['@babel/plugin-transform-typescript', {...}],
		['@babel/plugin-proposal-decorators', {version: '2022-03'}],
		['@babel/plugin-proposal-class-static-block'], // needed, or decorator output will currently not work in Safari.
	],

and off to the races! 🐎

@trusktr
Copy link
Contributor

trusktr commented Sep 15, 2022

For now, here's what the signature of a stage 3 decorator may look like to avoid type errors:

export function someDecorator(...args: any[]): any {
	const [value, {kind, ...etc}] = args as [DecoratedValue, DecoratorContext]
	console.log('stage 3!', value, kind, etc)

	if (kind === 'class') {
		// ...
	} else if (kind === 'field') {
		// ...
	} else (/*...etc...*/) {
		// ...
	}

	// ...
}

interface DecoratorContext {
	kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor'
	name: string | symbol
	access: Accessor
	private?: boolean
	static?: boolean
	addInitializer?(initializer: () => void): void
}

interface Accessor { 
    get?(): unknown
    set?(value: unknown): void
}

type Constructor<T = object, A extends any[] = any[], Static = {}> = (new (...a: A) => T) & Static

type DecoratedValue = Constructor | Function | Accessor | undefined

@pokatomnik
Copy link

Sorry for the offtopic question, but what will happen to stage3 decorators? Will they be removed from Typescript?

@rbuckton
Copy link
Member

rbuckton commented Nov 9, 2022

Sorry for the offtopic question, but what will happen to stage3 decorators? Will they be removed from Typescript?

I assume you mean the Stage 1 "experimental" decorators that are already in TypeScript? That will remain behind --experimentalDecorators for the foreseeable future as there are a number of decorator features that are not yet covered by the current Decorators proposal:

We also do not currently support decorated declare fields with Stage 3 decorators, as that would introduce significant additional complexity due to the timing of decorator application.

@rbuckton
Copy link
Member

rbuckton commented Nov 9, 2022

For now, here's what the signature of a stage 3 decorator may look like to avoid type errors:

[...]

The built-in lib definitions in #50820 contain types that will hopefully be able to help:

// class decorator
function MyClassDecorator<T extends new (...args: any) => any>(target: T, context: ClassDecoratorContext<T>) {
};

// method decorator
function MyMethodDecorator<T extends (...args: any) => any>(target: T, context: ClassMethodDecorator<unknown, T>) {
};

// or, a catch-all decorator
function MyDecorator(target: unknown, context: DecoratorContext) {
  switch (context.kind) {
    case "class": ...
    case "method": ...
    ...
  }
}

@pokatomnik
Copy link

@rbuckton, thank you, I mean ES decorators reached stage 3. And because of that, I believe they'll be implemented in the Typescript as well. And correct me if I'm wrong, but the newest decorators are not compatible with the good old "experimental". So I thought, Typescript will have to support them since a lot of frameworks are relying on them.
In other words, my question is "should I write a new code with the experimental decorators or will they be deprecated in about 3 years?"
Thanks in advance!

@ruojianll
Copy link

"should I write a new code with the experimental decorators or will they be deprecated in about 3 years?"

Hope there is adapter to update experimental decorators to stage 3 automatic.

@trusktr
Copy link
Contributor

trusktr commented Nov 12, 2022

@pokatomnik while experimental stage 1 decorators will be supported for some unknown amount of time, I believe we can assume that eventually non-experimental stage 3 decorators will land and we'll be able to rely on those.

If you want to prepare for this future, the best way to do that currently is to use tsc only for type checking, and use babel for transpiling stage 3 decorators today. Then it should be minimal effort later to switch to using just tsc for type checking and compiling.

I'm betting on stage 3 decorators with a setup like I mentioned in the above comment.

@trusktr
Copy link
Contributor

trusktr commented Nov 12, 2022

Hope there is adapter to update experimental decorators to stage 3 automatic.

I don't think TypeScript will do that (I don't think it has ever been done for any feature). Experimental stage 1 decorators and stage 3 decorators are highly incompatible. Start migrating!

@ruojianll
Copy link

Hope there is adapter to update experimental decorators to stage 3 automatic.

I don't think TypeScript will do that (I don't think it has ever been done for any feature). Experimental stage 1 decorators and stage 3 decorators are highly incompatible. Start migrating!

I will try, but it is a terrible work especially for package creators to update their user's code.

@jithujoshyjy
Copy link

jithujoshyjy commented Nov 15, 2022

A little nod about the metadata in decorators. tc39/proposal-type-annotations#159 can this be done in typescript?

@rbuckton
Copy link
Member

Hope there is adapter to update experimental decorators to stage 3 automatic.

I don't think TypeScript will do that (I don't think it has ever been done for any feature). Experimental stage 1 decorators and stage 3 decorators are highly incompatible. Start migrating!

No, we won't automatically adapt legacy decorators as that would require type-directed emit. Also, there is not 1:1 parity between legacy decorators and ECMAScript decorators, which I mentioned above.

It's possible to write a custom decorator adapter, though that would entail varying degrees of complexity depending on the decorator you are adapting. An adapter that provided full backwards compatibility with legacy decorators would likely incur a performance penalty, though it would be feasible:

// legacy.ts
@foo
@bar({ x: 1 })
class C {
  @baz method() {}
  @quxx field;
}

// native.ts
import { createDecoratorAdapter } from "./adapter";

const _ = createDecoratorAdapter();

@_ // <- last applied decorator
@_(foo)
@_(bar({ x: 1 })
class C {
  @_(baz) method() {}
  @_(quxx) field;
}

// adapter.ts
export function createDecoratorAdapter() {
  // TODO: returns a function that queues up each legacy decorator application
  // and performs legacy decorator evaluation at the end as part of a
  // final class decorator.
}

@rbuckton
Copy link
Member

Here's a rough implementation of a comprehensive createDecoratorAdapter(): https://gist.github.com/rbuckton/a464d1a0997bd3dab36c8b0caef0959a

NOTE: It's not designed to be intermingled with native decorators, as all decorator applications are queued until an adapted class decorator runs (or the adapter itself is used as a class decorator).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Committed The team has roadmapped this issue Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.