Mix your TypeScript code together with minimal explicit types.
npm install miximumMiximum provides three key features for building TypeScript applications:
- Designing and Composing Components - Type-safe dependency injection and composition system that eliminates boilerplate interfaces
- Emitting Events - Minimal event system with strong typing per event name
- Reactive State - Tiny mutable state store with subscriptions and derivations
Components are simple object factories: take a dependencies object, run setup, return a component instance.
import { design } from 'miximum';
const ConsoleLogger = design( ( { name }: { name: string } ) => {
const prefix = name.substring( 0, 5 ).toLowerCase();
return {
write: ( ...args: any[] ) => console.log( `[${prefix}]`, ...args )
};
} );
const loggerA = ConsoleLogger.create( { name: 'a' } );
const loggerB = ConsoleLogger.create( { name: 'b' } );
loggerA.write( 'test' ); // outputs: [a] test
loggerB.write( 'test' ); // outputs: [b] testThis is more than just functions with extra steps – components give you type-safe dependency injection without hand-writing interface types.
import { design, compose } from 'miximum';
const MemoryStore = design( () => {
const data = new Map<string, string>();
return {
set: ( key: string, value: string ) => data.set( key, value ),
get: ( key: string ) => data.get( key ),
remove: ( key: string ) => data.set( key, undefined )
};
} );
const RedisStore = design( () => {
const client = new Redis();
return {
set: ( key: string, value: string ) => client.set( key, value ),
get: ( key: string ) => client.get( key ),
remove: ( key: string ) => client.set( key, undefined )
};
} );
// MemoryStore defines the default implementation and the interface that other stores must match
const DocumentDatabase = design( MemoryStore, ( { set, get, remove } ) => {
return {
add( doc: { id: string, content: string } ) {
set( doc.id, JSON.stringify( doc ) );
},
get( id: string ) {
const doc = get( id );
return doc ? JSON.parse( doc ) : undefined;
},
remove
};
} );
// Uses MemoryStore implicitly since the dependency was not provided
const memoryDb = DocumentDatabase.create();
// Uses RedisStore explicitly since the dependency was provided
const redisDb = DocumentDatabase.create( RedisStore.create() );Use design() to define components. Use compose() to combine multiple components with matching dependencies.
When to use which:
design( setup ): Define a component with dependenciesdesign( parent, setup ): Extend a parent design with new setupdesign( parent, child ): Extend a parent design with another designcompose( [ A, B ] ): Merge a list of components into one instancecompose( { a: A, b: B } ): Map components to named keys
Composing a list of components merges all their instance properties into a single object. If multiple components define the same property, the value from the last component in the list takes precedence ("last-one-wins"). Proxy objects are used to ensure that composed instances correctly handle getters, setters, and primitive properties.
Composing a map of components (using an object) avoids property conflicts by nesting each component under its own key. This can be useful for organizing related functionality and doesn't incure the overhead of proxy objects.
Examples:
import { design, compose } from 'miximum';
// 1) Simple design
const Env = design( ( dep: { apiKey?: string } ) => ( {
apiKey: dep.apiKey ?? process.env.API_KEY
} ) );
// 2) Compose list (merged component instance)
const Posts = design( Env, ( { apiKey } ) => ( {
getPost: ( id: string ) => `post:${id}:${apiKey}`
} ) );
const Users = design( Env, ( { apiKey } ) => ( {
getUser: ( id: string ) => `user:${id}:${apiKey}`
} ) );
const Data = compose( [ Posts, Users ] );
// 3) Compose map (nested structure)
const Cache = design( () => ( {
set: ( k: string, v: string ) => void 0,
get: ( k: string ) => undefined as string | undefined
} ) );
const Services = compose( { data: Data, cache: Cache } );
// 4) Use composed parents as dependency
const App = design( Env, compose( { services: Services } ) );
// Create
const app = App.create();
// app has services, services has data and cache, data has getPost and getUser
const post = app.services.data.getPost( 'post-id' );Components provide methods for creating instances and modifying dependencies:
Create an instance of the component design by providing required dependencies. The method signature adapts based on the dependency type. If no dependencies are needed, create() can be called without arguments.
const Logger = design( ( dep: { prefix: string } ) => ( {
log: ( msg: string ) => `[${dep.prefix}] ${msg}`
} ) );
// With required dependencies
const logger = Logger.create( { prefix: 'INFO' } );
logger.log( 'hello' ); // outputs: [INFO] hello
// Without dependencies
const Simple = design( () => ( { value: 42 } ) );
const instance = Simple.create();Pre-fill specific dependency values and remove them from the required set:
const Database = design( ( dep: { host: string, port: number, ssl: boolean } ) => ( {
connect: () => `${dep.ssl ? 'https' : 'http'}://${dep.host}:${dep.port}`
} ) );
// Production database with sensible defaults
const ProductionDb = Database.with( { ssl: true, port: 5432 } );
const db = ProductionDb.create( { host: 'prod.example.com' } );
// Only requires 'host', ssl and port are pre-filledExtend the dependency type with additional properties:
const Logger = design( ( dep: { prefix: string } ) => ( {
log: ( msg: string ) => `[${dep.prefix}] ${msg}`
} ) );
// Add timestamp option
const ExtendedLogger = Logger.extend<{ timestamp: boolean }>();
// ExtendedLogger.create() now requires { prefix, timestamp }Emitters are objects that emit events with strong typing per event name.
import { createEmitter } from 'miximum';
// Define your event payloads
type Events = {
log: { message: string },
joined: { userId: string }
};
const emitter = createEmitter<Events>();
const unsubscribe = emitter.on( 'log', ( { message } ) => {
console.log( 'LOG', message );
} );
emitter.emit( 'log', { message: 'hello' } );
// Remove a listener
unsubscribe();
// Derive a filtered emitter
const errors = emitter.pick( 'log' );
errors.emit( 'log', { message: 'only logs flow here' } );Methods:
on(event, callback)- Subscribe to an event, returns unsubscribe functiononce(event, callback)- Subscribe once, auto-unsubscribes after first emitoff(event, callback?)- Remove specific listener or all listeners for an eventemit(event, payload)- Emit an event to all subscriberspick(...events)- Create a filtered emitter accepting only specified eventsomit(...events)- Create a filtered emitter excluding specified eventsderive(transform)- Create a derived emitter with transformed payloadsnext(event)- Return a promise that resolves on next emitclear()- Remove all listeners
States are mutable stores with subscription and derivation helpers.
import { createState } from 'miximum';
// Initial state
const state = createState( { count: 0 } );
// Read / write
state.get( 'count' );
state.set( 'count', 1 );
state.update( { count: 2 } );
state.update( ( prev ) => ( { count: prev.count + 1 } ) );
// Subscribe
const unsubscribe = state.subscribe( ( snapshot ) => {
console.log( snapshot.count );
} );
// Derive read-only state
const doubled = state.derive( ( prev ) => ( { value: prev.count * 2 } ) );
const value = doubled.get( 'value' );
unsubscribe();Methods:
get(key)- Read a value by keyset(key, value)- Set a value (undefined only allowed if type permits)update(changes)- Merge object or call updater functionentries()- Iterate all key-value pairstoObject()- Get a shallow copy of state as plain objectsubscribe(callback)- Listen for changes, returns unsubscribe functionderive(transform)- Create read-only derived state
Run the test suite:
npm testMIT