Utility Type: ConstructorParameters for Class Constructor Parameter Types
Introduction
Constructor signatures are a common place where types and runtime behavior meet. When you build factories, dependency injection containers, or wrapper utilities that must construct instances of classes generically, you frequently need the exact parameter list of a class constructor — including the tuple shape, types, and ordering. Without an easy way to extract that information, you end up duplicating types or losing type-safety by using any or unknown and manual casts.
TypeScript provides the built-in utility type ConstructorParameters
By the end of this long-form guide you'll be able to:
- Confidently extract constructor argument tuples and reuse them in type-safe factories.
- Compose ConstructorParameters
with tuple transforms, mapped types, and conditional types. - Handle abstract classes, new signatures, overloads, and edge-cases.
- Use Constructable helper types and write reusable utilities that accept "newable" types.
This article assumes an intermediate understanding of TypeScript's type system and will include multiple code examples, patterns, and troubleshooting tips so you can use ConstructorParameters
Background & Context
ConstructorParameters
The utility is most useful when you want to avoid repeating parameter tuples across your codebase: factories, wrappers, and DI containers can accept classes of different shapes while preserving correct types for construction. Under the hood, ConstructorParametersinfer keyword to match new signatures and extract the argument types. If T is not a constructable type, the utility yields never or a compile-time error, depending on how you use it.
Because it relies on tuple types and inference, ConstructorParametersinfer with functions and objects to understand the mechanism this utility leverages: see Using infer with Functions in Conditional Types and Using infer with Objects in Conditional Types — Practical Guide.
Key Takeaways
- ConstructorParameters
extracts a constructor's parameter types as a tuple. - It only works with constructable ("newable") types; otherwise, results can be
neveror error. - Combine it with tuple mapping,
InstanceType, andReturnType<T>to build factories and wrappers. - Be careful with overloaded constructors; explicit new signatures are safer.
- Use helper types such as
new (...args: ConstructorParameters<T>) => InstanceType<T>to reconstruct constructors.
Prerequisites & Setup
- TypeScript 3.1+ is required for ConstructorParameters
and many utility types (use a recent TypeScript version; 4.x+ recommended). - Familiarity with tuple types, mapped types, conditional types, and the
inferkeyword. - Basic project: Node.js, npm/yarn, a TypeScript config (tsconfig.json) set to at least "target": "ES2017" and "strict": true for best results.
To follow examples, create a minimal TypeScript project:
mkdir ts-constructor-params && cd ts-constructor-params npm init -y npm install --save-dev typescript @types/node npx tsc --init
Open the index.ts and try the code snippets as you read. Adjust tsconfig.json to enable strict to expose type errors and make the learning experience clearer.
Main Tutorial Sections
1) Basic Usage: Extracting Constructor Parameters
ConstructorParametersnew signature. Example:
class Logger {
constructor(private prefix: string, private level: number) {}
}
type LoggerCtorArgs = ConstructorParameters<typeof Logger>; // [string, number]Now LoggerCtorArgs is the tuple [string, number]. You can reuse this tuple in factories or helpers without repeating the argument types manually.
function create<T extends abstract new (...args: any) => any>(C: T, ...args: ConstructorParameters<T>) {
return new C(...args);
}
const logger = create(Logger, 'app', 3);This keeps create fully generic and type-safe for any constructable type.
2) Handling Non-Constructor Types and Guards
ConstructorParametersnever. To make safe utilities, constrain generics to the new signature:
type Newable = abstract new (...args: any) => any;
function makeInstance<T extends Newable>(C: T, ...args: ConstructorParameters<T>) {
return new C(...args);
}If you receive unknown types at runtime (e.g., from a plugin registry), validate or type-guard them before using ConstructorParameters. Because ConstructorParameters is purely a type-level construct, runtime checks will still be necessary when working with dynamic values.
For details on infer and conditional types used under the hood, the guides Using infer with Functions in Conditional Types and Using infer with Objects in Conditional Types — Practical Guide are useful references.
3) Abstract Classes and Interfaces with 'new' Signatures
Abstract classes and interfaces can be represented as new signatures. If you wish to extract constructor parameters from an abstract class, use typeof the class or constrain to a new signature:
abstract class BaseService {
constructor(public url: string) {}
}
type BaseCtorArgs = ConstructorParameters<typeof BaseService>; // [string]
interface ServiceCtor {
new (url: string): BaseService;
}
type FromInterface = ConstructorParameters<ServiceCtor>; // [string]Having new signatures in interfaces makes ConstructorParameters work seamlessly. This pattern is especially handy when designing plugin hosts or DI container APIs that accept abstract types instead of concrete implementations.
4) Using ConstructorParameters in Factories & DI
A common pattern is to implement a typed factory or DI container that constructs classes generically while preserving argument types:
class Container {
create<T extends abstract new (...args: any) => any>(C: T, ...args: ConstructorParameters<T>) {
// You might resolve dependencies here and spread them into args
return new C(...args) as InstanceType<T>;
}
}
const container = new Container();
const logger = container.create(Logger, 'svc', 1);Because create takes ...args: ConstructorParameters<T>, callers get full compile-time checks on the argument types, improving reliability of DI wiring without brittle manual type duplication.
5) Reconstructing Constructor Types (Newable Helpers)
Often you want to reconstruct a constructor type using ConstructorParameters
type Reconstruct<T extends abstract new (...args: any) => any> =
new (...args: ConstructorParameters<T>) => InstanceType<T>;
function wrap<T extends abstract new (...args: any) => any>(C: T): Reconstruct<T> {
return class extends (C as any) {
constructor(...args: any[]) {
super(...args);
// add behavior
}
} as any;
}This wrap retains the precise constructor parameter tuple of the original class, so code that uses the wrapped constructor remains type-safe.
6) Combining with Parameters and ReturnType
While ConstructorParameters
function factoryFromFunction<F extends (...args: any) => any>(fn: F) {
type FnParams = Parameters<F>;
// Use FnParams in other contexts
}When you want to extract returns after building instances or proxies, consider using ReturnType
7) Working with Overloaded Constructors
Overloaded constructors are tricky: TypeScript chooses a single signature when a type is used in certain contexts, typically the last overload signature in the declaration. That means ConstructorParametersnew signature or use an interface with a clear new signature:
class Multi {
constructor(a: string);
constructor(a: string, b: number);
constructor(a: any, b?: any) {}
}
// ConstructorParameters<typeof Multi> -> may be [a: any, b?: any]To preserve exact overload semantics, prefer explicit new signatures on interfaces, or provide a strongly-typed factory wrapper that uses conditional types to narrow parameter possibilities.
For advanced conditional and distribution behaviors, review our guide on Distributional Conditional Types in TypeScript.
8) Transforming Constructor Tuples into Options Objects
A practical pattern is converting a constructor parameter tuple into a named options object using tuple-to-object transforms. Use mapped types and recursive mapped transforms for this task:
type ParamNames = ['url', 'timeout'];
type TupleToObject<T extends readonly any[], Keys extends readonly PropertyKey[]> = {
[I in keyof T as Keys[I & number]]: T[I]
};
type Args = [string, number];
type Opts = TupleToObject<Args, ParamNames>; // { url: string; timeout: number }For deep tuple transformations and advanced mapped types see Recursive Mapped Types for Deep Transformations in TypeScript and Advanced Mapped Types: Key Remapping with Conditional Types.
9) Interoperating with Arrays, Tuples, and Rest Parameters
ConstructorParameters returns a tuple type that works with ... rest positions. When passing a tuple to spread into a constructor, use as const or explicit tuple types:
function instantiate<T extends abstract new (...args: any) => any>(C: T, args: ConstructorParameters<T>) {
return new C(...args);
}
const args = ['http://', 5000] as const;
instantiate(Logger, args as unknown as ConstructorParameters<typeof Logger>);The as const helps keep literal types; for complex transformations of tuple elements, see Using infer with Arrays in Conditional Types — Practical Guide.
10) Practical Wrappers: Proxies, Decorators, and Logging
You can build decorators and proxies for classes that preserve constructor signatures by relying on ConstructorParameters:
function withLogging<T extends abstract new (...args: any) => any>(C: T) {
return class extends (C as any) {
constructor(...args: ConstructorParameters<T>) {
console.log('creating', C.name, args);
super(...args);
}
} as unknown as Reconstruct<T>;
}
const LoggedLogger = withLogging(Logger);
const l = new LoggedLogger('svc', 2);This pattern keeps the wrapped constructor’s signature intact so callers get full type support while adding cross-cutting concerns such as logging or metrics.
Advanced Techniques
Once you're comfortable with the basics, you can apply ConstructorParametersinfer to build utilities that accept only constructors whose first parameter implements a specific interface. Or create a type that maps a constructor tuple to a Promise-wrapped version of the instance, useful for asynchronous factories:
type AsyncFactory<T extends abstract new (...args: any) => any> = (...args: ConstructorParameters<T>) => Promise<InstanceType<T>>;
You can also create specialized DI registration types that store constructor argument metadata using Utility Type: Record<K, T> for Dictionary Types to hold mapping from tokens to constructor param tuples. For recursive transforms and modeling complex deep changes, consult Recursive Conditional Types for Complex Type Manipulations and Distributional Conditional Types in TypeScript: A Practical Guide for patterns and performance trade-offs.
Performance tip: heavy use of deeply recursive conditional types can slow down TypeScript's type checker. Keep utility types focused and consider splitting very complex transforms into smaller, named helper types to reduce compile-time cost.
Best Practices & Common Pitfalls
- Always constrain generics that use ConstructorParameters
to a newsignature:T extends abstract new (...args: any) => anyto avoidneversurprises. - Prefer explicit
newsignatures (interfaces or type aliases) when your constructors have overloads — overloaded constructors can lead to ambiguous inferred parameter tuples. - Use
as constwhen preserving literal tuple elements for argument arrays to keep the most specific types. - Avoid extremely deep or recursive type-level transforms unless necessary: they can slow down TS compiler. When you need deep transforms, split logic into named helper types and test incrementally.
- Beware of runtime vs. type-level mismatch: ConstructorParameters
is type-level only. If your runtime values may not be constructors, validate them before constructing.
Troubleshooting:
- If you get unexpected
anytypes in tuples, check if the class was written in JavaScript or had implicitanyparameters. - If ConstructorParameters returns
never, ensure you passed a constructor type (usetypeof MyClassor anewinterface) and that your generic constraint allows new signatures.
Real-World Applications
- Dependency Injection (DI) containers: automatically infer and pass constructor arguments when wiring components while retaining type-safety.
- Test factories: generate instances for tests by using ConstructorParameters to create argument builders and default factories.
- Wrappers and decorators: preserve constructor signatures while augmenting behavior (logging, performance metrics, instrumentation).
- Plugin systems: accept class implementations from third-parties while validating and typing the constructor args.
For complex mapping of constructor tuple shapes into configuration objects for such systems, you might store those mappings in a dictionary with Utility Type: Record<K, T> for Dictionary Types to keep plugin metadata organized.
Conclusion & Next Steps
ConstructorParameters
Next steps: practice by converting an existing factory or container to use ConstructorParameters
Enhanced FAQ
Q: What exactly does ConstructorParametersclass Foo { constructor(a: string, b: number) {} }, ConstructorParameters<typeof Foo> is [string, number].
Q: Can I use ConstructorParameters on function constructors like new Function()?
A: You can only use ConstructorParameters on types that declare a construct signature (types that are "newable"). Plain functions without new signatures are not valid and will typically produce never unless you provide a new signature type for them.
Q: How does it differ from Parameters(...args: any) => any). ConstructorParametersnew) signatures. If you need function parameters, use Parameters
Q: What happens with overloaded constructors?
A: Overloaded constructors can be ambiguous: TypeScript often selects a single signature for inference (commonly the implementation signature or the last overload depending on context). Because of that, ConstructorParameters may not reflect all overloads. Prefer explicit new signatures on interfaces or single-signature constructors for reliable inference.
Q: Is ConstructorParameters safe to use in public library APIs?
A: Yes — when used carefully. Constrain your generics (T extends abstract new (...args: any) => any) and document expected class shapes. For libraries, prefer clear new signatures in types you export to avoid overload ambiguity and to provide consistent consumer experience.
Q: Can I transform the returned tuple into an object with named fields? A: Yes. You can use mapped types and key remapping to convert a tuple into a named object. For complex deep transformations, refer to Recursive Mapped Types for Deep Transformations in TypeScript.
Q: How do I reconstruct a constructor type from ConstructorParameterstype Reconstruct<T extends new (...args:any) => any> = new (...args: ConstructorParameters<T>) => InstanceType<T> to build a constructor type that preserves argument and instance types. This is useful for wrappers and proxies.
Q: What are common pitfalls when using ConstructorParameters
- Forgetting to constrain the generic to a
newsignature and gettingnever. - Overloaded constructors causing ambiguous tuples.
- Relying purely on types for runtime validation (ConstructorParameters
is compile-time only). - Excessive complex recursive types causing slow compile times.
Q: Can I use ConstructorParameters for classes written in JavaScript (JS)?
A: Type inference depends on your declaration files or @types. If the JS class has typed declarations (d.ts) or the project uses JSDoc with type annotations, it can work. Otherwise, TypeScript may fall back to any for parameters, making the tuple less useful.
Q: How does ConstructorParameters interact with InstanceType and ReturnType?
A: Use InstanceType
Q: Any recommended further reading?
A: To strengthen related knowledge, read about infer in conditional types (Using infer with Functions in Conditional Types), tuple and array inference (Using infer with Arrays in Conditional Types — Practical Guide), and advanced mapped/recursive transformations (Recursive Mapped Types for Deep Transformations in TypeScript, Advanced Mapped Types: Key Remapping with Conditional Types).
