question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Types for Decorators Design Sync, 10/28/2022

See original GitHub issue

This 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?
    • 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 when C 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?

    • 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 a string.
  • 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.

  • 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 of A1 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:open
  • Created a year ago
  • Reactions:12
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

2reactions
fatcerberuscommented, Nov 2, 2022

Image macro of duck saying “WAT”

Time for me to retire now and become a duck…

1reaction
Jack-Workscommented, Oct 31, 2022

What about this? Let the converted type be declared on the decorator instead of the class field.

declare function Await<T extends PromiseLike>(target: undefined, context: ClassFieldDecoratorContext): (value: T) => Awaited<T>;
declare function NonNull<T>(target: undefined, context: ClassFieldDecoratorContext): (value: T | undefined | null) => T;
declare function DependencyInject<T extends Injectable>(): (target: undefined, context: ClassFieldDecoratorContext) => (value: T) => T;

class T {
    @Await field
    //     ~~~~~ implicit any
    @Await field: number
    // contextual type to infer to Await<Promise<number>>?
    @Await<Promise<number>> field
    //                      ~~~~~ number
    @Await<Promise<number>> field: string
    //     ~~~~~~~~~~~~~~~       ~~~~~~~~ incompatible declaration

    @NonNull<string | null> field
    //                      ~~~~~ string
    @Service(MyClass) field
    //                ~~~~~ MyClass instance
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Decorator - Refactoring.Guru
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that...
Read more >
Decorator Design Pattern in Java Example - DigitalOcean
We need to have following types to implement decorator design pattern. Component Interface - The interface or abstract class defining the ...
Read more >
Documentation - Decorators - TypeScript
TypeScript Decorators overview. ... The TypeScript compiler will inject design-time type information using the @Reflect.metadata decorator.
Read more >
Decorator pattern - Wikipedia
In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found