Types for Decorators Design Sync, 10/28/2022
See original GitHub issueThis wasn’t a formal design meeting, but I figured it was worth taking notes for it.
Type Mutation for Decorators
Best Current Decorators Overview I Know Of: https://2ality.com/2022/10/javascript-decorators.html
Current Decorators Issue: https://github.com/microsoft/TypeScript/issues/48885 Ron’s Decorators PR: https://github.com/microsoft/TypeScript/pull/50820 Original “Decorator Mutation” Issue: https://github.com/microsoft/TypeScript/issues/4881
-
Want usages of a property to be a string.
declare function Stringify(target: undefined, context: ClassFieldDecoratorContext): (value: string | number) => string; class C { @Stringify x: number = 1; constructor() { this.x; // usage is a 'string' } method() { this.x // usage is a 'string' } }
- Potential for confusion - but we believe that people can “get it” either way after the initial learning hump.
- But declaration emit - sometimes our declaration emitter requires users to specify something explicitly to get things working.
- Does the annotation refer to the pre-decorated type, or the post-decorated type?
- If we use the annotation as the final type, you can use it as the contextual type of the decorator call in some case.
- Arguments for resulting type?
- Declaration emit is simplified
- Works well with the
isolatedDeclarations
scenario. - Can use a type assertion (
as
) for the expression type.
- Arguments for “current type”?
- Short circuiting circularities with type annotations is useful - type assertions still resolve the expression.
- Still may need to refer to intermediate types.
- What does
typeof C
refer to whenC
has any number of decorations?
-
Seems reasonable for fields; what about signatures?
-
Could declare overloads?
class C { method(x: Boxed<string>): void; @BoxFirstArgument method(s: string) { this.x; } }
-
But so if
method
is invoked somewhere else…then the signature is different?- Maybe.
-
So when do you need a separate method signature?
- When you want to (i.e. you should be able to)
- For so-far hypothetical
--isolateDeclarations
. - But also for possible
--noImplicitAny
issues.
-
Just weird that you could write
@BoxFirstArgument func: (s: Boxed<sring>) => void = (s: string) => {};
- Well. Yes.
-
-
Seems like annotation should reflect the final type?
-
What about a future with parameter decorators?
-
Need to be able to specify the type for callers, not the body of the function.
class C { constructor(@Stringify foo: number) { foo; // this is a string foo.toUpperCase(); // valid } } new C(123); // works new C("str"); // error
- Callers need to call with a
number
, body needs to witness astring
.
- Callers need to call with a
-
-
Type mutations can be observed from several places in decorator invocations (as they capture the original target types). Modeled via type parameters.
- Input - type of the declaration.
- Output - type given the current declaration.
- Final - final type of declaration
- This - final type of the constructor
-
There is a reason on top of ES standardization that we’ve avoided just adding the type modification behavior of decorators for years - all of this is entirely subtle.
-
A big part of the subtlety is that the decorator affects the containing class.
-
Can we avoid witnessing the entire evolution of the class?
- Each decorator may expect the output of the previously-running decorator.
-
The decorator can replace the class entirely with a function.
- Seems very questionable.
- Lots of stuff in the compiler probably fails when you say “must be a class”
- Bad-ish example: https://github.com/microsoft/TypeScript/issues/50751
-
Could pre-allocate unresolved types on every decorator invocation so that we can defer and force resolution only when necessary.
-
Would it help to be able to say decorators can fully change members, but the final resulting class must be a subtype?
- Not exactly - within the class, you might want to take advantage of introduced methods.
- Kind of like the “class mixing factory”.
-
What are the invariants in the compiler?
-
Single symbol for a single entity.
- A symbol’s resolution can be deferred, and we don’t know what the order of resolution will be.
- Symbol types don’t “evolve” - they are either resolved, resolving (temporary state to detect circularities), or complete.
-
Reasonable way to model this, but there needs to be a new symbol “kind” that doesn’t know what kind of declaration it’s resolving to.
- Kind of sounds like a special version of anonymous object types. Are they?
-
So this would look like:
// Order is specified by letter, then number in ascending order. @E2 @E1 class SomeClass { // decorator applied symbols // final "x" -> @C2 -> @C1 -> original "x" @C2 @C1 static x; @A2 @A1 static F() {} @D2 @D1 y; @B2 @B1 g() {} }
-
There is also
addInitializer
- Should not be able to apply mutations, but can witness the final type.
- The
addInitializer
ofA1
may require the type produced by@E2
.
-
So
final SomeClass = @E2 -> @E1 -> {@A2, @B2, @C2, D2}
-
-
Aside: is
SomeClass
TDZ?-
Method decorators can be functions and refer to the final incomplete state.
@Blah class C { @((t, c) => { // valid but questionable. SomeClass.x c.addInitializer(() => { // valid. SomeClass.x }) }) static x; }
-
-
Seems sound, but this seems to fall over in language service scenarios.
- Circularities can be triggered before type-checking by things like auto-completion or quick info.
-
Back to earlier topic - sometimes you want the annotation to be the original type so that you can infer for the decorator input.
- If the annotation is the final type, then the decorators can take an explicit type argument, or do return-type inference (which is an anti-pattern but 🤷🏻♂️).
- Parameters might need to be the output position too
- Seems strange.
-
Feel like we’re converging on ideas, but not 100% yet. Need more discussion.
-
The first thing we ship will likely be a version where we forbid type mutations - maybe things being a strict subtype.
Issue Analytics
- State:
- Created a year ago
- Reactions:12
- Comments:5 (3 by maintainers)
Top GitHub Comments
Time for me to retire now and become a duck…
What about this? Let the converted type be declared on the decorator instead of the class field.