Repository navigation
Support to method decorator that change the method signature #49229
Description
Activity
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');
Reacted by Thundercraft5, Gabriel Porto, Paul Kiddle, John Wright, Szymon Bretner, runjuu, Alex Ryan, PJ Eby, Gonçalo Fernandes, Jacob Sánchez and 10 more- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this feature
on Jun 2, 2022 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" ?
Reacted by Van Tigranyan, 吉凯, Dan Chu, TiPOS GmbH, Bela Bohlender, Paul Kiddle, John Wright, Szymon Bretner, Oussama Benhamed, David Ekeflod and 21 moreThis 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.
Reacted by AndriiDemchenko, Jialiang T., Alex Ryan, oddguan, Alex Patin, Adam, Valerie-June, PJ Eby, Segu, WillsterJohnsonAtZenesis and 14 moreI 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.
Reacted by Benedikt StrehleTo 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
nullorundefinedfrom 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.
Reacted by Thibaud Courtoison, Travis Fischer, Benedikt Strehle, Neil Baillie, Jonathan Hefner and snarbles2It 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();
Reacted by Sʜɪᴍᴜʀᴀ Yū, David Brito, Arthur Fontaine, Ethan Resnick and LuisHere'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 } }
Reacted by Manish Demblani, Benedikt Strehle, Jonathan Hefner and GerkinCompleted 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
methodstill uses(value: number) => number, while the observable type ofExample.prototype.methodis(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, andneverpreserve the incoming callable.- A callable-or-preserving result exposes the union of the replacement and incoming callables.
anyremainsany.- Invalid non-callable and
unknownresults retain their diagnostics and recover to the last valid callable, allowing later decorators to continue composing.
Under
strictNullChecks: false, explicitundefinedcan 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/undefinedresults - Overloaded source methods and overloaded replacement callables
- Generic methods and classes, explicit type arguments, aliases, and class expressions
- Polymorphic
this, static versus instanceThis, andcontext.static - Public, protected, TypeScript-private, and JavaScript
#privatemethods 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
- Should the observable method type compose bottom-to-top while the source signature remains responsible for body checking?
- Should type-changing decorators be automatic for eligible standard methods, or require an opt-in compiler option?
- Are the preservation, recovery, and conservative non-strict-null rules appropriate?
- Which intentional exclusions should remain in an initial upstream design?
- Given the TypeScript 7 repository transition, where and when should further design or implementation work happen?
Reacted by Kalos-S- Branch:
Suggestion
As the title said, I just started using typescript and I want to have a
methoddecorator that is able to change the method signature and havetscknew about the new signature.🔍 Search Terms
I have seen a lot of works around
classdecorators, like #4881 but nothing specifically on supporting the behavior I have described.✅ Viability Checklist
My suggestion meets these guidelines:
⭐ 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
tscsasys that there is an error in the last line, because it doesn't "know" thatC.methodhave a new signature, after applying thedecorator.💻 Use Cases
I'm implementing a simple method wrapper that add a parameter to do pre-checks on method invocation itself