import { type ContainerLike, type DenyInputKeys, type Factory, type IDIContainer, type MergedResolvers, type ResolvedDependencies, type ResolvedDependencyValue, type Resolvers, type StringLiteral } from './types.js';
/**
 * Dependency injection container
 */
export declare class DIContainer<ContainerResolvers extends ResolvedDependencies = {}> {
    protected resolvedDependencies: {
        [name in keyof ContainerResolvers]?: ResolvedDependencyValue;
    };
    protected resolvers: Resolvers<ContainerResolvers>;
    private readonly context;
    constructor();
    /**
     * Combines independently built containers into a single new container.
     *
     * This is the recommended way to wire a large dependency graph. Splitting the graph
     * into modules and composing them is dramatically cheaper to type-check than one long
     * `add` chain, because each module chain is type-checked against its own small
     * resolver map instead of the ever-growing combined one:
     *
     * // repositories.ts
     * export const repositories = new DIContainer().add('userRepository', () => new UserRepository());
     * // services.ts
     * export const services = new DIContainer().add('mailer', () => new Mailer());
     * // container.ts
     * const container = DIContainer.compose(repositories, services);
     *
     * Factories may depend on names provided by any of the composed containers — resolution
     * happens lazily against the composed container, so cross-module dependencies work at
     * runtime. Only the *types* of a module are limited to what that module declares; when a
     * module needs another module's dependencies to be visible at compile time, layer them
     * with `extend` instead.
     *
     * The inputs are left untouched: the composed container is a new instance.
     *
     * When several containers define the same name the last one wins at runtime, including
     * over a value the earlier container had already resolved. Note the types intersect rather
     * than overwrite, so a name defined twice with *different* types resolves to `never` — a
     * deliberate signal, since a scalable last-writer-wins type fold has to recurse per
     * container and trips TypeScript's depth limiter at ~50 of them. Prefer `update()` when a
     * replacement is intentional.
     * @param containers
     */
    static compose<T extends readonly ContainerLike[]>(...containers: T): IDIContainer<MergedResolvers<T>>;
    /**
     * Adds new dependency resolver to the container. If dependency with given name already exists it will throw an error.
     * Use update method instead. It will override existing dependency.
     * @param name
     * @param resolver
     */
    add<N extends string, V>(name: StringLiteral<DenyInputKeys<N, keyof ContainerResolvers>>, resolver: Factory<ContainerResolvers, V>): IDIContainer<ContainerResolvers & {
        [n in N]: V;
    }>;
    /**
     * Creates a new container instance with the same resolvers.
     *
     * Useful when you want to share a base container across different modules.
     * For example, you can define a base container with shared dependencies,
     * then clone it to create separate DI configurations for different bounded contexts.
     *
     * The cloned container is a new instance but retains all the original resolvers.
     */
    clone(): DIContainer<ContainerResolvers>;
    export(): ResolvedDependencies;
    /**
     * Extends container with given function. It will pass container as an argument to the function.
     * Function should return new container with extended resolvers.
     * It is useful when you want to split your container into multiple files.
     * You can create a file with resolvers and extend container with it.
     * You can also use it to create multiple containers with different resolvers.
     *
     * For example:
     *
     * const container = new DIContainer()
     * .extend(addValidators)
     *
     * export type DIWithValidators = ReturnType<typeof addValidators>;
     * export const addValidators = (container: DIWithDataAccessors) => {
     * return container
     * .add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c))
     * .add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c));
     * };
     * @param diConfigurationFactory
     */
    extend<E extends (container: IDIContainer<ContainerResolvers>) => IDIContainer>(diConfigurationFactory: E): ReturnType<E>;
    /**
     * Resolve dependency by name. Alternatively you can use property access to resolve dependency.
     * For example: const { a, b } = container;
     * @param dependencyName
     */
    get<Name extends keyof ContainerResolvers>(dependencyName: Name): ContainerResolvers[Name];
    /**
     * Checks if dependency with given name exists
     * @param name
     */
    has(name: string): boolean;
    hasResolvedDependency(name: string): boolean;
    /**
     * Merges other containers into this one. Resolved dependencies are merged as well.
     *
     * Accepts any number of containers, so a set of independently built modules can be
     * combined in a single call:
     *
     * base.merge(repositories, services, controllers)
     *
     * Combining modules this way is also much cheaper to type-check than one long
     * `add` chain — see docs/type-performance-plan.md.
     *
     * When several containers define the same name the last one wins at runtime, including over
     * an already-resolved value. The types intersect rather than overwrite, so the same name with
     * two different types resolves to `never` rather than the later type.
     *
     * This mutates and returns `this`; use `clone()` or the static `DIContainer.compose()`
     * when a separate instance is required.
     * @param containers
     */
    merge<T extends readonly ContainerLike[]>(...containers: T): IDIContainer<ContainerResolvers & MergedResolvers<T>>;
    /**
     * Updates existing dependency resolver. If dependency with given name does not exist it will throw an error.
     * In most cases you don't need to override dependencies and should use add method instead. This approach will
     * help you to avoid overriding dependencies by mistake.
     *
     * You may want to override dependency if you want to mock it in tests.
     * @param name
     * @param resolver
     */
    update<N extends keyof ContainerResolvers, V>(name: StringLiteral<N>, resolver: Factory<ContainerResolvers, V>): IDIContainer<{
        [n in N]: V;
    } & {
        [P in Exclude<keyof ContainerResolvers, N>]: ContainerResolvers[P];
    }>;
    protected setResolvers<CR extends ResolvedDependencies>(resolvers: Resolvers<CR>, resolvedDependencies: {
        [name in keyof CR]: ResolvedDependencyValue;
    }): void;
    private addContainerProperty;
    /**
     * Sets value to the container
     */
    private setValue;
}
