This TypeScript error occurs when a decorator factory doesn't return a function as expected. Decorator factories are functions that return decorator functions, and forgetting the return statement or returning undefined will cause this error. Fix it by ensuring your factory returns a proper decorator function.
A decorator factory in TypeScript is a function that returns another function (the actual decorator). This pattern allows you to configure decorators with custom parameters before they're applied to classes, methods, properties, or parameters. The "Decorator factory must return a function" error appears when you create a decorator factory that doesn't return a function, often because you forgot the return statement, returned undefined, or returned a non-function value. TypeScript expects this pattern: 1. **Decorator factory** (outer function): Takes configuration parameters and returns a decorator 2. **Decorator** (returned function): Takes the target (class, method, etc.) and applies the decoration logic When the factory fails to return a function, TypeScript cannot apply the decorator at compile time, resulting in this error. This is different from the experimental decorators syntax and applies to both legacy decorators (with experimentalDecorators flag) and the modern decorators introduced in TypeScript 5.0.
A decorator factory must return a function that accepts the decorator target. Check that your return statement is present:
// WRONG - no return statement (returns undefined)
function MyDecorator(config: string) {
function decorator(target: any) {
console.log(config, target);
}
// Missing return!
}
// CORRECT - returns the decorator function
function MyDecorator(config: string) {
return function decorator(target: any) {
console.log(config, target);
};
}
// Usage
@MyDecorator("my-config")
class MyClass {}The factory must always return a function with the appropriate signature for the decorator type (class, method, property, or parameter).
Arrow functions with curly braces require explicit return statements. Verify your syntax:
// WRONG - curly braces without return
const MyDecorator = (config: string) => {
(target: any) => {
console.log(config, target);
}
};
// CORRECT - explicit return
const MyDecorator = (config: string) => {
return (target: any) => {
console.log(config, target);
};
};
// ALSO CORRECT - implicit return with parentheses
const MyDecorator = (config: string) => (target: any) => {
console.log(config, target);
};If using single-expression arrow functions, wrap the returned function in parentheses for implicit return.
Ensure all code paths in your factory return a function:
// WRONG - some paths return undefined
function MyDecorator(enabled: boolean) {
if (enabled) {
return function(target: any) {
console.log("Enabled", target);
};
}
// Missing return for else case!
}
// CORRECT - all paths return a function
function MyDecorator(enabled: boolean) {
return function(target: any) {
if (enabled) {
console.log("Enabled", target);
}
};
}
// ALSO CORRECT - explicit returns for all cases
function MyDecorator(enabled: boolean) {
if (enabled) {
return function(target: any) {
console.log("Enabled", target);
};
}
return function(target: any) {
console.log("Disabled", target);
};
}Every execution path must return a valid decorator function, even if it's a no-op.
Different decorator types require different function signatures. Ensure your returned function matches the decorator type:
// Class decorator
function ClassDecorator(options: any) {
return function<T extends { new (...args: any[]): {} }>(constructor: T) {
// Modify or return constructor
return constructor;
};
}
// Method decorator
function MethodDecorator(options: any) {
return function(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
// Modify descriptor
return descriptor;
};
}
// Property decorator
function PropertyDecorator(options: any) {
return function(target: any, propertyKey: string | symbol) {
// Define property metadata
};
}
// Parameter decorator
function ParameterDecorator(options: any) {
return function(
target: any,
propertyKey: string | symbol,
parameterIndex: number
) {
// Store parameter metadata
};
}Use the correct signature based on where you're applying the decorator.
Decorator factories cannot be async because decorators must return synchronously:
// WRONG - async factory returns a Promise, not a function
async function MyDecorator(config: string) {
const data = await fetchConfig(config);
return function(target: any) {
console.log(data, target);
};
}
// WRONG - trying to await in decorator usage (not supported)
@await MyDecorator("config")
class MyClass {}
// CORRECT - fetch synchronously or use a different pattern
function MyDecorator(config: string) {
// Fetch data inside the decorator, not the factory
return function(target: any) {
fetchConfig(config).then(data => {
console.log(data, target);
});
};
}
// CORRECT - pass data directly if already available
function MyDecorator(data: any) {
return function(target: any) {
console.log(data, target);
};
}Move async operations inside the returned decorator function, not in the factory itself.
Understand when to use @decorator vs @decorator():
// Plain decorator (no factory) - use without parentheses
function SimpleDecorator(target: any) {
console.log("Decorating", target);
}
@SimpleDecorator // No parentheses
class ClassA {}
// Decorator factory - requires parentheses
function ConfigurableDecorator(config: string) {
return function(target: any) {
console.log(config, target);
};
}
@ConfigurableDecorator("my-config") // With parentheses and arguments
class ClassB {}
// WRONG - mixing them up
@SimpleDecorator("config") // Error: SimpleDecorator doesn't return a function
class ClassC {}
@ConfigurableDecorator // Error: This returns the factory, not the decorator
class ClassD {}Use parentheses only when calling a factory. Omit them for direct decorators.
### Legacy vs Modern Decorators
TypeScript has two decorator implementations:
Legacy Decorators (Stage 2 proposal, pre-TypeScript 5.0):
Enabled with experimentalDecorators: true in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}function OldStyleDecorator(config: string) {
return function(target: any) {
// target is the class constructor
};
}Modern Decorators (Stage 3, TypeScript 5.0+):
Native ECMAScript decorators with different signatures:
function NewStyleDecorator(config: string) {
return function(target: any, context: ClassDecoratorContext) {
// context provides metadata about the decorated element
console.log(context.kind); // "class", "method", "getter", etc.
};
}The error can occur if you're mixing decorator styles or using the wrong signature for your TypeScript version.
### Decorator Factory Pattern Best Practices
1. Type safety with generics:
function TypedDecorator<T extends { new (...args: any[]): {} }>(options: any) {
return function(constructor: T) {
return class extends constructor {
// Extend with type safety
};
};
}2. Configuration validation:
interface DecoratorOptions {
enabled: boolean;
level: "debug" | "info" | "error";
}
function ValidatedDecorator(options: DecoratorOptions) {
// Validate options in factory
if (!options || typeof options.enabled !== "boolean") {
throw new Error("Invalid decorator options");
}
return function(target: any) {
// Use validated options
};
}3. Composable decorators:
function compose(...decoratorFactories: Array<(options: any) => Function>) {
return function(options: any) {
return function(target: any) {
decoratorFactories.forEach(factory => {
factory(options)(target);
});
};
};
}### Common Gotchas
1. Returning the result instead of the function:
// WRONG - returns the execution result (undefined)
function MyDecorator(config: string) {
return (function(target: any) {
console.log(config, target);
})(); // Immediately invoked - returns undefined!
}
// CORRECT - returns the function itself
function MyDecorator(config: string) {
return function(target: any) {
console.log(config, target);
};
}2. Method descriptor return values:
For method decorators, if you return a value, it becomes the new property descriptor. For property decorators, return values are ignored:
function MethodDecorator(config: string) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
// Returning undefined is valid - keeps original descriptor
// Returning an object replaces the descriptor
return {
...descriptor,
writable: false
};
};
}3. Metadata with reflect-metadata:
Using reflect-metadata requires careful handling:
import "reflect-metadata";
function InjectDecorator(serviceId: string) {
return function(target: any, key: string, index: number) {
const existingParams = Reflect.getMetadata("injections", target, key) || [];
existingParams[index] = serviceId;
Reflect.defineMetadata("injections", existingParams, target, key);
};
}### Debugging Decorator Factories
Enable verbose logging to understand execution flow:
function DebugDecorator(label: string) {
console.log(`[Factory] Creating decorator with label: ${label}`);
return function(target: any, context?: any) {
console.log(`[Decorator] Applying to:`, target);
console.log(`[Context]`, context);
// Your decorator logic
};
}This helps identify whether the factory is being called and what it returns.
### TypeScript Version Considerations
Check your TypeScript version for decorator support:
npx tsc --version- Pre-5.0: Use experimentalDecorators: true for any decorators
- 5.0+: Modern decorators work without flags, but legacy syntax needs the flag
- Mixed codebases: Can support both with feature detection
For libraries, document which decorator style you're using to avoid consumer confusion.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript