镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Support to method decorator that change the method signature #49229

Description

@ZeeD

Suggestion

As the title said, I just started using typescript and I want to have a method decorator that is able to change the method signature and have tsc knew about the new signature.

🔍 Search Terms

I have seen a lot of works around class decorators, like #4881 but nothing specifically on supporting the behavior I have described.

✅ 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.

⭐ Suggestion

If not for other things, I would like to understand if there is some way to set the types of decoratorto signal the new decorated function signature.

📃 Motivating Example

As a minimal example, look at

function decorator(_target: any, _propertyKey: string, descriptor: PropertyDescriptor): void {
    const method = descriptor.value;
    descriptor.value = function wrapper(a: number, b: number, c: number): void {
        // here you have access to a, b, c
        console.log(a, b, c);
        return method.call(this, a, b, c);
    }
}

class C {
    @decorator
    method(a: number, b: number): void {
        // here you have access to a, b
        console.log(a, b);
    }
}

new C().method(1, 2, 3); // here you pass a, b and c

tsc sasys that there is an error in the last line, because it doesn't "know" that C.method have a new signature, after applying the decorator.

💻 Use Cases

I'm implementing a simple method wrapper that add a parameter to do pre-checks on method invocation itself

Activity

  1. gund commented on Jun 1, 2022

    @gund

    It is usually an anti-pattern as decorators are meant to be transparent - meaning that no changes to method signature must be done by the decorator and only a behavior may be added/changed (like doing validation/logging).

    As for your case it seems like you are adding some extra "private" arguments that your consumer and receiver must not see/access and only your decorator can see and possibly some privileged part of the code?

    In this case you can create a shortcut mapped type that adds new argument on the fly so you can cast your method in place just before invoking it. Pseudo TS could look something like this:

    type AnyFunction = (...args: any[]) => any;
    
    type ExtendedMethod<TMethod extends AnyFunction> = (
      // Add extra argument of type "string" at the end
      ...args: [...Parameters<TMethod>, string]
    ) => any;
    
    type ExtendedObject<T> = {
      // Extend all methods on the object
      [P in keyof T]: T[P] extends AnyFunction ? ExtendedMethod<T[P]> : T[P];
    };
    
    function asExtended<T>(obj: T) {
      return obj as ExtendedObject<T>;
    }
    
    class Example {
      method(a: number, b: number) {}
    }
    
    // Standard call
    new Example().method(1, 2);
    
    // Extended call that knows about extra argument
    asExtended(new Example()).method(1, 2, '3');
  2. soffyo commented on Oct 3, 2022

    @soffyo

    I don't agree that decorators should only be used for validation/logging. I think that adding methods and properties to classes via decorators is one of their main goals and capabilities and typescript should be able to access to the added signatures. If there is such wish to let decorators remain a 'hidden' thing, maybe the best option would be to have a flag in compiler options like "DecoratorsChangeTypes" ?

  3. ssalbdivad commented on Jul 25, 2023

    @ssalbdivad

    This feature would be a big QOL improvement for TS developers wanting to rely on composition over inheritance in their classes.

    It would be very natural to allow the wrapper method to have its own signature which, to me, would substantially increase the utility of existing decorator functionality.

  4. finom commented on May 22, 2024

    @finom

    I hope it's going to be implemented right after decorators reach stage 4. Currently it doesn't make a lot of sense to be able to change signatures while decorators aren't standardised, unfortunately.

  5. sybereal commented on Nov 7, 2024

    @sybereal

    To add onto the validation topic, I believe that is actually a very convenient case for changing the signature. When working with a library like Zod, it is not rare for the result of the parsing operation to have a different type than the input, even if it's just removing null or undefined from property types.

    In my case, I wanted to write a decorator to do exactly that, i.e., running a method's input through a provided Zod schema first before calling the method itself, and then passing the result to the method. Since this is a use case that happens in many different places in the code, I thought that a decorator would be a great abstraction to avoid having to replicate the manual parse invocation at all usage sites.

  6. Jamesernator commented on Jan 19, 2025

    @Jamesernator

    It would be particularly helpful for decorators for decorators to be able to change the class type as well, for example for adding extra methods:

    function using<This, T>(field: undefined, ctx: ClassFieldDecoratorContext<This, T>) {
        // add disposable ctx.access to metadata
    }
    
    function disposable<Class extends new(...args: any) => any>(cls: Class, ctx: ClassDecoratorContext<Class>) {
        return class extends cls {
            [Symbol.dispose](): void {
                // dispose disposable fields collected in metadata
            }
        }
    }
    
    @disposable
    class Foo {
        @using
        readonly field = createSomeDisposable();
    }
    
    // Currently a type error even though it works
    using foo = new Foo();
  7. Arlen22 commented on Apr 18, 2025

    @Arlen22

    Here's another example where it would be very useful. I'm surprised this isn't supported already.

    function validate<S extends ZodTypeAny, R>(schema: S) {
      return function (originalMethod: (arg: z.output<S>) => R, context: ClassMethodDecoratorContext) {
        return function(this: any, input: any) {
          return originalMethod.call(this, schema.parse(input))
        }
      };
    }
    
    class Test {
      @validate(z => z.object({
        value: z.string(),
      }))
      test(input){
        // input.value
      }
    }
  8. ayden94 commented on Jul 31, 2026

    @ayden94

    Completed implementation evidence

    I completed a TypeScript-Go/Corsa implementation of type-changing standard method decorators. This is an experimental language extension and implementation artifact, not a claim that the semantics have been accepted for upstream TypeScript.

    • Branch: prototype/decorator-method-final-type
    • Current implementation tip: b52b692c
    • Scope: 17 focused commits covering checker behavior, declaration emit, JavaScript/JSDoc, incremental updates, and language-service lifecycles

    Proposed semantic model

    The source method signature and the decorated member type have separate roles:

    • The method body is checked against its source signature.
    • Standard method decorators compose bottom-to-top, matching runtime application order.
    • Each decorator receives the callable produced by the previous stage as both its value argument and the callable in ClassMethodDecoratorContext.
    • The final composed callable becomes the observable member type.

    For example:

    function acceptText(
      original: (value: number) => number,
      _context: ClassMethodDecoratorContext,
    ): (value: string) => number {
      return function (this: unknown, value: string) {
        return original.call(this, Number(value));
      };
    }
    
    class Example {
      @acceptText
      method(value: number): number {
        return value * 2;
      }
    }
    
    new Example().method("21"); // number

    The body of method still uses (value: number) => number, while the observable type of Example.prototype.method is (value: string) => number.

    Stage-result rules

    • A callable result replaces the incoming callable.
    • Callable unions and overloaded callable results preserve all supported call signatures and callable facets.
    • void, undefined, and never preserve the incoming callable.
    • A callable-or-preserving result exposes the union of the replacement and incoming callables.
    • any remains any.
    • Invalid non-callable and unknown results retain their diagnostics and recover to the last valid callable, allowing later decorators to continue composing.

    Under strictNullChecks: false, explicit undefined can disappear during type normalization. The implementation therefore tracks fallback provenance through aliases, conditional types, inferred returns, explicitly typed identifier expressions, and uncertain indexed-access forms. Recursive analysis is bounded; indeterminate cases preserve the incoming callable rather than unsafely narrowing it away.

    Supported scope

    The implementation covers:

    • Direct decorators, decorator factories, and stacked decorators
    • Callable unions and callable-or-void/undefined results
    • Overloaded source methods and overloaded replacement callables
    • Generic methods and classes, explicit type arguments, aliases, and class expressions
    • Polymorphic this, static versus instance This, and context.static
    • Public, protected, TypeScript-private, and JavaScript #private methods without widening access
    • Callable properties, construct signatures, and string/number index signatures
    • Declaration emit of the final observable type
    • Checked JavaScript and JSDoc-authored decorators
    • Checker query-order determinism
    • Incremental declaration updates
    • Language-service quick-info and diagnostic refresh after edits

    Declaration emit

    Declaration emit uses method syntax when one ordinary call signature completely represents the final type. It uses property syntax when method syntax would lose information such as unions, overload sets, any, construct signatures, properties, or index signatures.

    For checked JavaScript, a member whose public type changes does not reuse stale type-bearing JSDoc. Unchanged JavaScript members and TypeScript API documentation retain their existing documentation behavior.

    Intentional exclusions

    This implementation does not apply decorator-driven member-type mutation to:

    • Legacy decorators
    • Non-method decorators, including fields, accessors, classes, and parameters
    • Abstract, ambient, or otherwise bodyless methods
    • isolatedDeclarations
    • Unchecked JavaScript

    These are explicit boundaries of this implementation, not assertions that a future language design cannot support them.

    Verification

    The branch includes focused checker, compiler, declaration, runtime, incremental, and Fourslash coverage for the supported behavior and exclusions. In particular, it tests overload composition, callable facets, generic/alias instantiation, both conditional-union orders, non-strict-null fallback provenance, query-order independence, incremental edits, and language-service edits.

    The current implementation passes:

    • Focused checker/compiler/declaration/runtime tests
    • Focused race tests
    • Full build
    • Full npx hereby test
    • Lint with 0 issues
    • Formatting checks for implementation and test sources
    • Independent goal, QA, code-quality, security, and context reviews

    Decisions needed for an upstream design

    1. Should the observable method type compose bottom-to-top while the source signature remains responsible for body checking?
    2. Should type-changing decorators be automatic for eligible standard methods, or require an opt-in compiler option?
    3. Are the preservation, recovery, and conservative non-strict-null rules appropriate?
    4. Which intentional exclusions should remain in an initial upstream design?
    5. Given the TypeScript 7 repository transition, where and when should further design or implementation work happen?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions