TypeScript Integration
HexDI is designed for TypeScript and leverages its type system for compile-time validation. This guide covers type inference, utilities, and best practices.
Zero-Annotation Design
HexDI requires no explicit type annotations in most cases. TypeScript infers everything:
// No type annotations needed!
const UserServiceAdapter = createAdapter({
provides: UserServicePort,
requires: [LoggerPort, DatabasePort],
lifetime: "scoped",
factory: deps => {
// TypeScript knows: deps = { Logger: Logger; Database: Database }
deps.Logger.log("Creating user service");
return {
getUser: async id => {
return deps.Database.query("SELECT * FROM users WHERE id = ?", [id]);
},
};
},
});
How Type Inference Works
Port Type Inference
port<Service>()({ name: 'Name' }) captures both the name (as a literal type) and service type:
const LoggerPort = port<Logger>()({ name: "Logger" });
// TypeScript sees:
// LoggerPort: DirectedPort<Logger, 'Logger', 'outbound'>
The name becomes a literal type, not just string.
Adapter Type Inference
createAdapter() infers types from the configuration object:
const adapter = createAdapter({
provides: LoggerPort, // infers TProvides
requires: [DatabasePort], // infers TRequires as tuple
lifetime: "singleton", // infers literal 'singleton'
factory: deps => ({
/* ... */
}),
});
// TypeScript sees:
// adapter: Adapter<typeof LoggerPort, typeof DatabasePort, 'singleton'>
GraphBuilder Type Accumulation
Each .provide() accumulates type information:
const builder1 = GraphBuilder.create();
// builder1: GraphBuilder<never, never>
const builder2 = builder1.provide(LoggerAdapter);
// builder2: GraphBuilder<typeof LoggerPort, never>
const builder3 = builder2.provide(UserServiceAdapter);
// builder3: GraphBuilder<
// typeof LoggerPort | typeof UserServicePort,
// typeof LoggerPort | typeof DatabasePort
// >
Container Type Inference
Container knows which ports can be resolved:
const container = createContainer({ graph, name: "App" });
// container: Container<typeof LoggerPort | typeof DatabasePort | ...>
const logger = container.resolve(LoggerPort);
// logger: Logger (inferred from Port)
container.resolve(UnknownPort);
// Error: Argument of type 'typeof UnknownPort' is not assignable...
Type Utilities
HexDI exports utilities for extracting type information.
Port Utilities
import { port, type InferService, type InferPortName, type InferPortDirection } from "@hex-di/core";
const LoggerPort = port<Logger>()({ name: "Logger" });
// Extract service type from port
type LoggerServiceType = InferService<typeof LoggerPort>;
// LoggerServiceType = Logger
// Extract port name
type LoggerPortName = InferPortName<typeof LoggerPort>;
// LoggerPortName = 'Logger'
// Extract direction
type LoggerDirection = InferPortDirection<typeof LoggerPort>;
// LoggerDirection = 'outbound'
Adapter Utilities
import { InferAdapterProvides, InferAdapterRequires, InferAdapterLifetime } from "@hex-di/graph";
// Extract what adapter provides
type Provides = InferAdapterProvides<typeof UserServiceAdapter>;
// Provides = typeof UserServicePort
// Extract what adapter requires
type Requires = InferAdapterRequires<typeof UserServiceAdapter>;
// Requires = typeof LoggerPort | typeof DatabasePort
// Extract lifetime
type Life = InferAdapterLifetime<typeof UserServiceAdapter>;
// Life = 'scoped'
GraphBuilder Utilities
import { InferGraphProvides, InferGraphRequires } from "@hex-di/graph";
const builder = GraphBuilder.create().provide(LoggerAdapter).provide(UserServiceAdapter);
// What ports are provided
type Provided = InferGraphProvides<typeof builder>;
// Provided = typeof LoggerPort | typeof UserServicePort
// What ports are required (before satisfaction)
type Required = InferGraphRequires<typeof builder>;
// Required = typeof LoggerPort | typeof DatabasePort
Container Utilities
import { InferContainerProvides, ServiceFromContainer, IsResolvable } from "@hex-di/runtime";
// Extract what container can provide
type Provides = InferContainerProvides<typeof container>;
// Provides = typeof LoggerPort | typeof DatabasePort | ...
// Get service type for a port from container
type LoggerType = ServiceFromContainer<typeof container, typeof LoggerPort>;
// LoggerType = Logger
// Check if port is resolvable
type CanResolve = IsResolvable<typeof container, typeof LoggerPort>;
// CanResolve = true
Creating Type-Safe Collections
AppPorts Type
Define a union type of all ports in your application:
// In ports.ts
export const LoggerPort = port<Logger>()({ name: "Logger" });
export const DatabasePort = port<Database>()({ name: "Database" });
export const UserServicePort = port<UserService>()({ name: "UserService" });
// Union of all ports
export type AppPorts = typeof LoggerPort | typeof DatabasePort | typeof UserServicePort;
Type-Safe React Hooks
Use the AppPorts type with React:
import { createTypedHooks } from "@hex-di/react";
import type { AppPorts } from "./ports";
// Hooks are now typed to only accept AppPorts
const { usePort, ContainerProvider } = createTypedHooks<AppPorts>();
// In components
function MyComponent() {
const logger = usePort(LoggerPort); // Works - LoggerPort is in AppPorts
// const unknown = usePort(UnknownPort); // Error - not in AppPorts
}
Compile-Time Error Messages
HexDI provides readable compile-time errors.
Missing Dependency Error
const graph = GraphBuilder.create()
.provide(UserServiceAdapter) // requires Logger, Database
.build();
Error message:
Type '"ERROR[HEX008]: Missing adapters for Logger | Database. Call .provide() first."'
is not assignable to type 'Graph<...>'.
The error shows exactly which ports are missing with an actionable fix message.
Duplicate Provider Error
const graph = GraphBuilder.create().provide(LoggerAdapter).provide(AnotherLoggerAdapter); // same port!
Error message:
Type '"ERROR[HEX001]: Duplicate adapter for \'Logger\'. Fix: Remove one .provide() call,
or use .override() for child graphs."' is not assignable to type 'GraphBuilder<...>'.
Invalid Resolution Error
container.resolve(UnregisteredPort);
Error message:
Argument of type 'Port<Unknown, "Unknown">' is not assignable to
parameter of type 'typeof LoggerPort | typeof DatabasePort | ...'.
Type-Level Dependency Tracking
HexDI tracks dependencies at the type level using TypeScript's conditional types.
ResolvedDeps Type
The factory function's deps parameter is typed using ResolvedDeps:
// Internal type (for understanding)
type ResolvedDeps<TRequires> = {
[P in TRequires as InferPortName<P>]: InferService<P>;
};
// Example
type MyDeps = ResolvedDeps<typeof LoggerPort | typeof DatabasePort>;
// MyDeps = { Logger: Logger; Database: Database }
Dependency Validation Types
import { UnsatisfiedDependencies, IsSatisfied } from "@hex-di/graph";
type Provided = typeof LoggerPort;
type Required = typeof LoggerPort | typeof DatabasePort;
// What's missing?
type Missing = UnsatisfiedDependencies<Provided, Required>;
// Missing = typeof DatabasePort
// Is everything satisfied?
type Satisfied = IsSatisfied<Provided, Required>;
// Satisfied = false
Advanced Type Patterns
Extracting Port Type from Graph
import type { Graph } from "@hex-di/graph";
// Extract TProvides from a graph
type AppPorts = typeof appGraph extends Graph<infer P> ? P : never;
Type-Safe Service Lookup
Create type-safe lookup functions:
function getService<P extends AppPorts>(container: Container<AppPorts>, port: P): InferService<P> {
return container.resolve(port);
}
const logger = getService(container, LoggerPort);
// logger: Logger
Conditional Types for Lifetime
type IsSingleton<A> = InferAdapterLifetime<A> extends "singleton" ? true : false;
type Test1 = IsSingleton<typeof LoggerAdapter>; // true
type Test2 = IsSingleton<typeof UserAdapter>; // false (scoped)
Best Practices
1. Let TypeScript Infer
Avoid explicit type annotations when possible:
// Good - TypeScript infers
const adapter = createAdapter({
provides: LoggerPort,
requires: [],
lifetime: "singleton",
factory: () => ({ log: console.log }),
});
// Unnecessary - explicit type
const adapter: Adapter<typeof LoggerPort, never, "singleton"> = createAdapter({
// ...
});
2. Use typeof for Port Types
When you need to reference a port's type:
// Good
function logWithPort(port: typeof LoggerPort) {
/* ... */
}
// Also good - using AppPorts union
function resolveAny(port: AppPorts) {
/* ... */
}
3. Export AppPorts Type
Always export a union type of all ports:
// ports.ts
export type AppPorts = typeof LoggerPort | typeof DatabasePort | typeof UserServicePort;
4. Use const for Literal Types
The const modifier preserves literal types:
// createPort uses const internally
const LoggerPort = port<Logger>()({ name: "Logger" });
// Name is literal 'Logger', not string
5. Validate Graph at Module Level
Build graphs at module level to catch errors early:
// graph.ts - errors caught at compile time
export const appGraph = GraphBuilder.create()
.provide(LoggerAdapter)
.provide(UserServiceAdapter)
.build(); // Compile error if invalid
Troubleshooting
"Type 'never' is not assignable"
Usually means a dependency chain is broken:
const graph = GraphBuilder.create()
.provide(UserServiceAdapter) // requires Logger
.build(); // Error: never is not assignable...
Solution: Add missing adapters.
"Cannot find name 'typeof'"
Make sure you're using typeof with the port value:
// Wrong
type MyPort = LoggerPort;
// Right
type MyPort = typeof LoggerPort;
"Expected 0 arguments, but got 1"
The .build() method expects an error argument when dependencies are missing:
// This is HexDI's way of forcing you to see the error
.build() // Shows: Expected 1 argument, got 0
// The argument type shows what's missing
Advanced Topics
For a deep dive into HexDI's type-level programming techniques:
- Type-Level Programming Patterns - Understand the advanced TypeScript patterns used for compile-time validation
Next Steps
- Explore Testing Strategies
- Learn about Error Handling
- See API Reference for complete documentation