@doeixd/make-with
Version:
Lightweight function application utilities
1,080 lines • 42.1 kB
TypeScript
/**
* @file A functional utility library for creating powerful, immutable, and chainable APIs.
* It provides tools for partial application and function composition, enabling elegant state
* management patterns.
*
* @version 0.0.5
* @license MIT
*/
declare const IS_CHAINABLE: unique symbol;
/** A utility type representing a collection of methods for a given subject. */
type Methods<S extends object = object> = Record<string, MethodFunction<S>>;
/** A function that operates on a subject and returns some result. */
type MethodFunction<S extends object> = (subject: S, ...args: readonly unknown[]) => unknown;
/**
* Helper type to detect if a method return type should be treated as chainable.
* Chainable methods return exactly the subject type or an extension of it.
*/
type IsChainableReturn<R, S> = R extends S ? S extends R ? true : R extends S & infer _Ext ? true : false : false;
/**
* The return type of a `provideTo` (`makeWith`) call. It correctly infers the
* return types for both regular methods and those marked as chainable.
*/
type ChainableApi<Fns extends Methods<S>, S extends object> = {
[K in keyof Fns as K extends typeof IS_CHAINABLE ? never : K]: Fns[K] extends (s: S, ...args: infer A) => infer R ? IsChainableReturn<R, S> extends true ? (...args: A) => ChainableApi<Fns, S> : (...args: A) => R : never;
};
/** A utility type that infers a simple, non-chainable API shape. Used by `makeLayered`. */
type BoundApi<S extends object, F extends Methods<S>> = {
[K in keyof F]: F[K] extends (subject: S, ...args: infer A) => infer R ? (...args: A) => R : never;
};
/** A function layer that receives the current API and returns methods to bind to it. */
type LayerFunction<CurrentApi extends object> = (currentApi: CurrentApi) => Methods<CurrentApi>;
/**
* Symbol to mark objects as composable, similar to IS_CHAINABLE.
*/
declare const IS_COMPOSABLE: unique symbol;
/** Enhanced LayeredApiBuilder type that supports both object and function layers. */
type LayeredApiBuilder<CurrentApi extends object> = {
(): CurrentApi;
<EnhancerFns extends Methods<CurrentApi>>(enhancerFns: EnhancerFns): LayeredApiBuilder<CurrentApi & BoundApi<CurrentApi, EnhancerFns>>;
<LayerFn extends LayerFunction<CurrentApi>>(layerFn: LayerFn): LayeredApiBuilder<CurrentApi & BoundApi<CurrentApi, ReturnType<LayerFn>>>;
};
export declare class LayeredError extends Error {
}
/**
* Partially applies a value to a set of functions, returning new functions with the value pre-applied.
* This is the simplest tool in the library, useful for reducing repetition when a group of
* functions all need the same initial argument.
*
* @template S The type of the subject to be partially applied.
* @param subject The value to partially apply to each function (e.g., a config object).
* @returns A higher-order function that takes multiple functions and returns a new array of
* functions, each with the `subject` pre-applied.
*
* @example
* const config = { user: 'admin', retries: 3 };
* const [fetchData, deleteData] = _with(config)(
* (cfg, path) => `Fetching ${path} for ${cfg.user}...`,
* (cfg, id) => `Deleting ${id} with ${cfg.retries} retries...`
* );
* fetchData('/items'); // "Fetching /items for admin..."
*/
export declare function _with<S>(subject: S): <Fs extends ((subject: S, ...args: any[]) => any)[]>(...fns: Fs) => { [K in keyof Fs]: Fs[K] extends (subject: S, ...args: infer A) => infer R ? (...args: A) => R : never; };
/**
* Normalizes a collection of functions into a consistent key-value object.
* It's a key utility for building APIs, as it accepts functions in two convenient formats:
* an array of named functions, or a single object of functions.
*
* @param fnsOrObj Either an array of named functions or a single object.
* @returns An object where keys are function names and values are the functions themselves.
* @throws {Error} If inputs are invalid (non-functions, unnamed anonymous functions, or duplicate names).
*
* @example
* // From named functions (name is used as key)
* function greet(name) { return `Hello, ${name}`; }
* const greeters = make(greet); // { greet: [Function: greet] }
*
* // From an object (keys are preserved)
* const math = make({ add: (a, b) => a + b }); // { add: [Function: add] }
*/
export declare function make<F extends (...args: any[]) => any>(...fns: F[]): Record<string, F>;
export declare function make<Obj extends Methods>(obj: Obj): Obj;
/**
* Marks a set of functions as "chainable" for use with `provideTo` (`makeWith`).
* A chainable function is a state updater that, when called, returns a new API instance
* bound to the new state, enabling fluent method calls.
*
* @param fnsOrObj An object of state-updating functions, or an array of named state-updating functions.
* @returns The same object of functions, but tagged with a special symbol for `provideTo` to recognize.
*
* @example
* // From an object
* const chainableMath = rebind({
* add: (state, n) => ({ ...state, value: state.value + n }),
* multiply: (state, n) => ({ ...state, value: state.value * n })
* });
*
* // From named functions
* function increment(state) { return { ...state, count: state.count + 1 }; }
* function decrement(state) { return { ...state, count: state.count - 1 }; }
* const chainableCounters = rebind(increment, decrement);
*/
export declare function rebind<Obj extends Methods>(obj: Obj): Obj;
export declare function rebind<Fs extends Array<(...args: any[]) => any>>(...fns: Fs): Record<string, Fs[number]>;
/**
* Creates a fluent API from a subject (state/config) and a collection of functions.
* It partially applies the subject to each function. If the methods object was wrapped
* with `makeChainable`, calling its methods will return a new, fully-formed API
* bound to the resulting state. This is the core function for state management patterns.
*
* @template S The type of the subject (state).
* @param subject The initial state to bind to the functions.
* @returns A higher-order function that takes a map of methods and returns the final API.
*
* @example
* const initialState = { count: 0, name: 'Counter' };
*
* // Non-chainable methods (getters, utilities)
* const getters = {
* get: (state) => state.count,
* getName: (state) => state.name
* };
*
* // Chainable methods (state updaters)
* const updaters = makeChainable({
* increment: (state) => ({ ...state, count: state.count + 1 }),
* setName: (state, name) => ({ ...state, name })
* });
*
* const counterAPI = makeWith(initialState)(updaters);
* const newAPI = counterAPI.increment().setName('Updated Counter');
*
* const readOnlyAPI = makeWith(initialState)(getters);
* const currentCount = readOnlyAPI.get(); // 0
*/
export declare function makeWith<S extends object>(subject: S): <Fns extends Methods<S>>(functionsMap: Fns) => ChainableApi<Fns, S>;
/**
* Creates a composable object where methods can access previous methods with the same name.
* Automatically handles both regular and chainable methods - the previous method always
* returns the appropriate result (state for chainable, actual result for regular).
*
* @template T The type of the composable methods object.
* @param methods An object where each method receives the previous method as its last parameter.
* @returns A marked object that can be used in `makeLayered` for composition.
*
* @example
* // Composing regular methods
* const api = makeLayered({ count: 0 })
* ({
* get: (s) => s.count,
* increment: (s) => ({ count: s.count + 1 })
* })
* (compose({
* get: (s, prevGet) => {
* const value = prevGet(s); // Returns number
* console.log('Current count:', value);
* return value;
* }
* }))
* ();
*
* @example
* // Composing chainable methods (automatically handled)
* const api = makeLayered({ count: 0 })
* (makeChainable({
* increment: (s) => ({ count: s.count + 1 }),
* add: (s, amount) => ({ count: s.count + amount })
* }))
* (compose({
* increment: (s, prevIncrement) => {
* console.log('Before increment:', s.count);
* const newState = prevIncrement(s); // Always returns state object
* console.log('After increment:', newState.count);
* return newState; // Returns state, becomes chainable automatically
* },
* add: (s, amount, prevAdd) => {
* if (amount < 0) {
* console.warn('Adding negative amount:', amount);
* amount = Math.abs(amount);
* }
* return prevAdd(s, amount); // Returns state object
* }
* }))
* ();
*
* @example
* // Mixed composition - some methods chainable, some not
* const userAPI = makeLayered({ users: [], count: 0 })
* (makeChainable({
* addUser: (s, user) => ({ ...s, users: [...s.users, user] })
* }))
* ({
* getUserCount: (s) => s.users.length,
* validateUser: (s, user) => user.name && user.email
* })
* (compose({
* addUser: (s, user, prevAdd) => {
* if (!s.validateUser(user)) throw new Error('Invalid user');
* return prevAdd(s, { ...user, id: crypto.randomUUID() }); // Returns state
* },
* getUserCount: (s, prevGetCount) => {
* const count = prevGetCount(s); // Returns number
* console.log(`Total users: ${count}`);
* return count;
* }
* }))
* ();
*/
export declare function compose<T extends Record<string, MethodFunction<any>>>(methods: T): T & {
[IS_COMPOSABLE]: true;
};
/**
* Merges multiple method objects with later objects taking precedence, or creates a curried
* merger function for partial application. This enhanced version supports both immediate merging
* and functional composition patterns.
*
* @template T The type of method objects to merge.
* @param objects Method objects to merge, in order of precedence (later objects override earlier).
* @returns A single merged object, or a curried function for further merging.
*
* @example
* // Direct merging (original behavior)
* const baseMethods = { get: (s) => s.value, set: (s, v) => ({ value: v }) };
* const extensions = { increment: (s) => ({ value: s.value + 1 }) };
* const validation = { set: (s, v) => v >= 0 ? ({ value: v }) : s }; // Override set
*
* const allMethods = merge(baseMethods, extensions, validation);
* const api = makeWith({ value: 0 })(allMethods);
*
* @example
* // Curried usage for extension patterns
* const addDefaults = merge({ role: 'user', active: true });
* const withAuth = merge({ isAuthenticated: (s) => !!s.token });
*
* const userMethods = addDefaults(withAuth({ login: (s, token) => ({ ...s, token }) }));
*
* @example
* // Building reusable extensions
* const withTimestamp = merge({
* addTimestamp: (s) => ({ ...s, createdAt: Date.now() }),
* updateTimestamp: (s) => ({ ...s, updatedAt: Date.now() })
* });
*
* const withValidation = merge({
* validate: (s, rules) => rules.every(rule => rule(s))
* });
*
* // Compose multiple extensions
* const enhancedAPI = makeWith(initialState)(
* withValidation(withTimestamp(baseMethods))
* );
*
* @example
* // Conditional merging with currying
* const createUserAPI = (isAdmin: boolean) => {
* const base = { getProfile: (s) => s.profile };
* const adminMethods = isAdmin ? { deleteUser: (s, id) => ({ ...s, deleted: [...s.deleted, id] }) } : {};
* return makeWith(initialState)(merge(base)(adminMethods));
* };
*
* @example
* // Chaining extensions functionally
* const processUser = (baseUser) =>
* merge({ id: crypto.randomUUID() })(
* merge({ createdAt: Date.now() })(
* merge({ role: 'user' })(baseUser)
* )
* );
*/
export declare function merge<T extends Methods>(...objects: T[]): T;
export declare function merge<T extends Methods>(firstObject: T): <U extends Methods>(...additionalObjects: U[]) => T & U;
/** Type representing a property descriptor that can be used as a merge key */
type MergePropertyDescriptor = {
key: string | symbol;
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
};
/** Type for merge result - either success with merged object or failure with error details */
type MergeResult<T> = {
success: true;
data: T;
} | {
success: false;
failures: Array<[string | symbol, unknown]>;
};
/** Type for merge definition functions */
type MergeDefinition<T> = {
[K in keyof T]?: (objA: Pick<T, K>, objB: Pick<T, K>, key: K) => T[K] | {
error: unknown;
};
};
/** Type for tuple-based merge definitions */
type TupleMergeDefinition<T extends Record<string | symbol, any>> = Array<[
keyof T | MergePropertyDescriptor,
(objA: any, objB: any, key: keyof T) => T[keyof T] | {
error: unknown;
}
]>;
/** Helper to get property descriptors for objects */
export declare function getKeyDescriptions<T extends object, U extends object>(objA: T, objB: U): Array<[string | symbol, PropertyDescriptor, PropertyDescriptor]>;
/**
* Creates a type-safe, auto-curried merger function that can merge objects according to custom merge strategies.
* Supports both object-based and tuple-based merge definitions with comprehensive error handling.
*
* @template T The type of objects to be merged.
* @param mergeDefinition An object where each key maps to a function that defines how to merge that property.
* @returns An auto-curried function that merges objects or returns detailed failure information.
*
* @example
* // Basic usage with merge definition object
* interface User {
* name: string;
* age: number;
* tags: string[];
* }
*
* const userMerger = createMerger<User>({
* name: (a, b, key) => a.name || b.name,
* age: (a, b, key) => Math.max(a.age || 0, b.age || 0),
* tags: (a, b, key) => [...(a.tags || []), ...(b.tags || [])]
* });
*
* const user1 = { name: "Alice", age: 25, tags: ["admin"] };
* const user2 = { name: "", age: 30, tags: ["user"] };
*
* const result = userMerger(user1, user2);
* if (result.success) {
* console.log(result.data); // { name: "Alice", age: 30, tags: ["admin", "user"] }
* }
*
* @example
* // Auto-currying support
* const partialMerger = userMerger(user1);
* const result2 = partialMerger(user2); // Same result as above
*
* @example
* // Error handling
* const strictMerger = createMerger<User>({
* name: (a, b, key) => a.name === b.name ? a.name : { error: "Name mismatch" },
* age: (a, b, key) => a.age > 0 && b.age > 0 ? Math.max(a.age, b.age) : { error: "Invalid age" }
* });
*
* const badResult = strictMerger({ name: "Alice", age: -1, tags: [] }, { name: "Bob", age: 30, tags: [] });
* if (!badResult.success) {
* console.log(badResult.failures); // [["name", "Name mismatch"], ["age", "Invalid age"]]
* }
*/
export declare function createMerger<T extends Record<string | symbol, any>>(mergeDefinition: MergeDefinition<T>): {
(objA: Partial<T>): (objB: Partial<T>) => MergeResult<T>;
(objA: Partial<T>, objB: Partial<T>): MergeResult<T>;
};
/**
* Creates a merger using tuple-based definitions for advanced property descriptor handling.
*
* @template T The type of objects to be merged.
* @param tupleDefinitions Array of tuples where each tuple contains a key/descriptor and merge function.
* @returns An auto-curried function that merges objects according to the tuple definitions.
*
* @example
* // Using property descriptors
* const descriptorMerger = createMerger<{ value: number; meta: string }>([
* ["value", (a, b, key) => (a.value || 0) + (b.value || 0)],
* [{ key: "meta", enumerable: true }, (a, b, key) => `${a.meta || ""}_${b.meta || ""}`]
* ]);
*
* const obj1 = { value: 10, meta: "first" };
* const obj2 = { value: 20, meta: "second" };
* const result = descriptorMerger(obj1, obj2);
* // result.data = { value: 30, meta: "first_second" }
*/
export declare function createMerger<T extends Record<string | symbol, any>>(tupleDefinitions: TupleMergeDefinition<T>): {
(objA: Partial<T>): (objB: Partial<T>) => MergeResult<T>;
(objA: Partial<T>, objB: Partial<T>): MergeResult<T>;
};
/** Type for proxy handler function result */
type ProxyHandlerResult<T> = T | {
error: unknown;
} | undefined;
/** Type for proxy handler function */
type ProxyHandler<S extends Record<string | symbol, any>> = (state: S, methodName: string | symbol, ...args: unknown[]) => ProxyHandlerResult<any>;
/** Type for proxy handler utility function */
type ProxyHandlerUtil<S extends Record<string | symbol, any>> = (state: S, methodName: string | symbol, ...args: unknown[]) => ProxyHandlerResult<any>;
/** Lens getter function type */
type LensGetter<S, T> = (state: S) => T;
/** Lens setter function type */
type LensSetter<S, T> = (state: S, focused: T) => S;
/**
* Utility function for common get/set pattern in createProxy.
* Handles getXxx() and setXxx() method patterns automatically.
*
* @template S The type of the state object.
* @param state The current state.
* @param methodName The method being called.
* @param args The arguments passed to the method.
* @returns The result of the get/set operation or undefined if pattern doesn't match.
*
* @example
* const userAPI = createProxy(getSet);
* // Automatically provides: getName(), setName(value), getAge(), setAge(value), etc.
*/
export declare function getSet<S extends Record<string | symbol, any>>(state: S, methodName: string | symbol, ...args: unknown[]): ProxyHandlerResult<any>;
/**
* Utility function that wraps another proxy handler to ignore case in method names.
*
* @template S The type of the state object.
* @param handler The proxy handler to wrap.
* @returns A new handler that normalizes method names to lowercase.
*
* @example
* const userAPI = createProxy(ignoreCase(getSet));
* // Now getName(), getname(), GETNAME() all work the same way
*/
export declare function ignoreCase<S extends Record<string | symbol, any>>(handler: ProxyHandlerUtil<S>): ProxyHandlerUtil<S>;
/**
* Utility function that wraps another proxy handler to strip special characters from method names.
*
* @template S The type of the state object.
* @param handler The proxy handler to wrap.
* @returns A new handler that removes special characters from method names.
*
* @example
* const userAPI = createProxy(noSpecialChars(getSet));
* // Now get_name(), get-name(), get$name() all become getname()
*/
export declare function noSpecialChars<S extends Record<string | symbol, any>>(handler: ProxyHandlerUtil<S>): ProxyHandlerUtil<S>;
/**
* Utility function that combines multiple proxy handlers, trying each in order until one returns a result.
*
* @template S The type of the state object.
* @param handlers Array of proxy handlers to try in order.
* @returns A combined handler that delegates to the first matching handler.
*
* @example
* const userAPI = createProxy(fallback([
* customMethods,
* getSet,
* (state, method) => `Method ${method} not found`
* ]));
*/
export declare function fallback<S extends Record<string | symbol, any>>(handlers: ProxyHandlerUtil<S>[]): ProxyHandlerUtil<S>;
/**
* Creates a dynamic API using ES6 Proxy that generates methods on-the-fly based on a handler function.
* This enables highly flexible APIs where methods are created dynamically based on state shape or naming patterns.
*
* @template S The type of the state object.
* @param handler Function that receives (state, methodName, ...args) and returns the result or new state.
* @returns A function that takes initial state and returns a dynamic API with auto-generated methods.
*
* @example
* // Basic usage with built-in getSet utility
* interface User {
* name: string;
* age: number;
* email: string;
* }
*
* const userAPI = createProxy<User>(getSet)({
* name: "Alice",
* age: 25,
* email: "alice@example.com"
* });
*
* // Methods are generated automatically:
* const name = userAPI.getName(); // "Alice"
* const updated = userAPI.setAge(26); // Returns new API with age: 26
* const email = updated.getEmail(); // "alice@example.com"
*
* @example
* // Composing utilities for flexible method generation
* const flexibleAPI = createProxy<User>(
* ignoreCase(noSpecialChars(getSet))
* )({ name: "Bob", age: 30, email: "bob@test.com" });
*
* // All these work the same way:
* flexibleAPI.getName(); // Standard
* flexibleAPI.getname(); // Ignore case
* flexibleAPI.get_name(); // No special chars
* flexibleAPI.GET_NAME(); // Both combined
*
* @example
* // Custom handler with business logic
* interface Counter {
* count: number;
* step: number;
* }
*
* const counterAPI = createProxy<Counter>((state, method, ...args) => {
* const methodStr = String(method);
*
* if (methodStr === 'increment') {
* return { ...state, count: state.count + state.step };
* }
*
* if (methodStr === 'decrement') {
* return { ...state, count: state.count - state.step };
* }
*
* if (methodStr === 'reset') {
* return { ...state, count: 0 };
* }
*
* if (methodStr.startsWith('add')) {
* const amount = args[0] as number;
* return { ...state, count: state.count + amount };
* }
*
* // Fallback to getSet for other methods
* return getSet(state, method, ...args);
* })({ count: 0, step: 1 });
*
* const result = counterAPI
* .increment() // count: 1
* .increment() // count: 2
* .add(5) // count: 7
* .setStep(2) // step: 2
* .increment(); // count: 9
*
* @example
* // Error handling with validation
* const validatedAPI = createProxy<User>((state, method, ...args) => {
* const result = getSet(state, method, ...args);
*
* // Add validation for setters
* if (String(method).startsWith('set') && result && typeof result === 'object') {
* if ('age' in result && (result as any).age < 0) {
* return { error: 'Age cannot be negative' };
* }
* if ('email' in result && !(result as any).email.includes('@')) {
* return { error: 'Invalid email format' };
* }
* }
*
* return result;
* })({ name: "Alice", age: 25, email: "alice@example.com" });
*
* const badResult = validatedAPI.setAge(-5); // Returns { error: 'Age cannot be negative' }
* const goodResult = validatedAPI.setAge(26); // Returns new API with age: 26
*/
export declare function createProxy<S extends Record<string | symbol, any>>(handler: ProxyHandler<S>): (initialState: S) => any;
/**
* Creates a lens that focuses method operations on a specific slice of state.
* This enables building APIs that operate on nested state structures while maintaining
* type safety and immutability patterns.
*
* @template S The type of the full state object.
* @template T The type of the focused state slice.
* @param getter Function that extracts the focused slice from the full state.
* @param setter Function that updates the full state with a new focused slice.
* @returns A function that takes methods and returns focused versions of those methods.
*
* @example
* // Basic lens usage for nested state
* interface AppState {
* user: {
* name: string;
* email: string;
* preferences: {
* theme: string;
* notifications: boolean;
* };
* };
* posts: Post[];
* ui: UIState;
* }
*
* // Create a lens focused on the user slice
* const userLens = createLens<AppState, AppState['user']>(
* state => state.user,
* (state, user) => ({ ...state, user })
* );
*
* // Methods that operate on the user slice
* const userMethods = {
* updateName: (user, name: string) => ({ ...user, name }),
* updateEmail: (user, email: string) => ({ ...user, email }),
* getName: (user) => user.name,
* getEmail: (user) => user.email
* };
*
* // Create API focused on user slice
* const appAPI = makeWith(appState)(userLens(userMethods));
*
* // Operations automatically work on the user slice
* const newAppState = appAPI.updateName("Alice").updateEmail("alice@example.com");
* // Full app state is updated, but methods only see/modify user slice
*
* @example
* // Deeply nested lens with preferences
* const preferencesLens = createLens<AppState, AppState['user']['preferences']>(
* state => state.user.preferences,
* (state, prefs) => ({
* ...state,
* user: { ...state.user, preferences: prefs }
* })
* );
*
* const prefMethods = {
* setTheme: (prefs, theme: string) => ({ ...prefs, theme }),
* toggleNotifications: (prefs) => ({ ...prefs, notifications: !prefs.notifications }),
* getTheme: (prefs) => prefs.theme
* };
*
* const prefsAPI = makeWith(appState)(preferencesLens(prefMethods));
* const updated = prefsAPI.setTheme("dark").toggleNotifications();
*
* @example
* // Chainable lens with makeChainable
* const chainableUserLens = createLens<AppState, AppState['user']>(
* state => state.user,
* (state, user) => ({ ...state, user })
* );
*
* const chainableAPI = makeWith(appState)(
* chainableUserLens(makeChainable({
* setName: (user, name: string) => ({ ...user, name }),
* setEmail: (user, email: string) => ({ ...user, email }),
* clearEmail: (user) => ({ ...user, email: "" })
* }))
* );
*
* // Fluent chainable API that focuses on user slice
* const result = chainableAPI
* .setName("Bob")
* .setEmail("bob@example.com")
* .clearEmail();
*
* @example
* // Lens composition for complex state management
* interface BlogState {
* posts: { id: string; title: string; content: string; author: string }[];
* authors: { id: string; name: string; email: string }[];
* currentPost: string | null;
* }
*
* // Lens for posts array
* const postsLens = createLens<BlogState, BlogState['posts']>(
* state => state.posts,
* (state, posts) => ({ ...state, posts })
* );
*
* // Lens for current post (returns single post or null)
* const currentPostLens = createLens<BlogState, BlogState['posts'][0] | null>(
* state => state.currentPost ? state.posts.find(p => p.id === state.currentPost) || null : null,
* (state, post) => post ? {
* ...state,
* posts: state.posts.map(p => p.id === post.id ? post : p)
* } : state
* );
*
* const blogAPI = makeLayered(blogState)
* (postsLens({
* addPost: (posts, post) => [...posts, { ...post, id: crypto.randomUUID() }],
* removePost: (posts, id: string) => posts.filter(p => p.id !== id)
* }))
* (currentPostLens({
* updateTitle: (post, title: string) => post ? { ...post, title } : null,
* updateContent: (post, content: string) => post ? { ...post, content } : null
* }))
* ();
*
* @example
* // Array lens for working with specific array elements
* const createArrayLens = <S, T>(
* getter: (state: S) => T[],
* setter: (state: S, array: T[]) => S,
* index: number
* ) => createLens<S, T | undefined>(
* state => getter(state)[index],
* (state, item) => {
* const array = getter(state);
* if (item === undefined) return setter(state, array.filter((_, i) => i !== index));
* const newArray = [...array];
* newArray[index] = item;
* return setter(state, newArray);
* }
* );
*
* // Focus on first post
* const firstPostLens = createArrayLens(
* (state: BlogState) => state.posts,
* (state, posts) => ({ ...state, posts }),
* 0
* );
*
* const firstPostAPI = makeWith(blogState)(firstPostLens({
* setTitle: (post, title: string) => post ? { ...post, title } : undefined,
* getTitle: (post) => post?.title || 'No post'
* }));
*/
export declare function createLens<S extends object, T>(getter: LensGetter<S, T>, setter: LensSetter<S, T>): <M extends Record<string, (focused: T, ...args: any[]) => any>>(methods: M) => Methods<S>;
/** Type for value validator function */
type ValueValidator<T = any> = (value: T) => boolean;
/** Type for fallback chain builder */
type FallbackChainBuilder<T extends Record<string | symbol, any>> = {
(): T;
(fallbackObject: Partial<T>): FallbackChainBuilder<T>;
};
/**
* Creates a fallback chain proxy that traverses multiple objects to find valid values.
* Uses a layered API similar to makeLayered for building fallback chains, with a final
* proxy that intelligently falls through objects until a valid value is found.
*
* @template T The type of the primary object and fallback objects.
* @param primaryObject The main object that will receive writes and be checked first for reads.
* @param validator Optional function to determine if a value is valid (defaults to non-null/undefined check).
* @returns A builder function for chaining fallback objects.
*
* @example
* // Basic fallback chain with default validator
* interface Config {
* apiUrl?: string;
* timeout?: number;
* retries?: number;
* debug?: boolean;
* }
*
* const userConfig: Config = { apiUrl: "https://api.user.com" };
* const teamConfig: Config = { timeout: 5000, retries: 3 };
* const defaultConfig: Config = {
* apiUrl: "https://api.default.com",
* timeout: 10000,
* retries: 1,
* debug: false
* };
*
* const config = withFallback(userConfig)
* (teamConfig)
* (defaultConfig)
* ();
*
* console.log(config.apiUrl); // "https://api.user.com" (from user)
* console.log(config.timeout); // 5000 (from team)
* console.log(config.retries); // 3 (from team)
* console.log(config.debug); // false (from default)
*
* // Writes go to the primary object
* config.timeout = 15000;
* console.log(userConfig.timeout); // 15000
*
* @example
* // Custom validator for non-empty strings
* const isNonEmptyString = (value: any): boolean =>
* typeof value === 'string' && value.trim().length > 0;
*
* const userPrefs = { name: "", theme: "dark" };
* const defaults = { name: "Anonymous", theme: "light", lang: "en" };
*
* const prefs = withFallback(userPrefs, isNonEmptyString)
* (defaults)
* ();
*
* console.log(prefs.name); // "Anonymous" (user's empty string is invalid)
* console.log(prefs.theme); // "dark" (user's value is valid)
* console.log(prefs.lang); // "en" (only in defaults)
*
* @example
* // Complex fallback chain with environment configuration
* interface AppConfig {
* database: {
* host?: string;
* port?: number;
* ssl?: boolean;
* };
* cache: {
* enabled?: boolean;
* ttl?: number;
* };
* features: {
* auth?: boolean;
* logging?: boolean;
* };
* }
*
* const envConfig: AppConfig = {
* database: { host: process.env.DB_HOST },
* cache: { enabled: true }
* };
*
* const localConfig: AppConfig = {
* database: { host: "localhost", port: 5432 },
* features: { auth: true, logging: true }
* };
*
* const prodDefaults: AppConfig = {
* database: { host: "prod-db", port: 5432, ssl: true },
* cache: { enabled: false, ttl: 3600 },
* features: { auth: true, logging: false }
* };
*
* const appConfig = withFallback(envConfig)
* (localConfig)
* (prodDefaults)
* ();
*
* // Nested property access works through the fallback chain
* console.log(appConfig.database.host); // From env or local or prod
* console.log(appConfig.database.ssl); // From prod (only defined there)
* console.log(appConfig.features.logging); // From local (overrides prod)
*
* @example
* // Validator for positive numbers
* const isPositiveNumber = (value: any): boolean =>
* typeof value === 'number' && value > 0;
*
* const userSettings = { volume: -1, brightness: 0.8 };
* const systemDefaults = { volume: 0.5, brightness: 1.0, contrast: 0.7 };
*
* const settings = withFallback(userSettings, isPositiveNumber)
* (systemDefaults)
* ();
*
* console.log(settings.volume); // 0.5 (user's -1 is invalid)
* console.log(settings.brightness); // 0.8 (user's value is valid)
* console.log(settings.contrast); // 0.7 (only in defaults)
*
* @example
* // Dynamic fallback chain building
* const createConfigChain = (environment: 'dev' | 'staging' | 'prod') => {
* const baseConfig = { app: 'MyApp', version: '1.0.0' };
* const builder = withFallback(baseConfig);
*
* if (environment === 'dev') {
* return builder
* ({ debug: true, logging: 'verbose' })
* ({ apiUrl: 'http://localhost:3000' })
* ();
* }
*
* if (environment === 'staging') {
* return builder
* ({ debug: false, logging: 'info' })
* ({ apiUrl: 'https://staging-api.example.com' })
* ();
* }
*
* return builder
* ({ debug: false, logging: 'error' })
* ({ apiUrl: 'https://api.example.com' })
* ();
* };
*
* const devConfig = createConfigChain('dev');
* console.log(devConfig.debug); // true
* console.log(devConfig.apiUrl); // 'http://localhost:3000'
*/
export declare function withFallback<T extends Record<string | symbol, any>>(primaryObject: T, validator?: ValueValidator): FallbackChainBuilder<T>;
/**
* An enhanced version of `makeWith` that automatically composes methods with the same name.
* When multiple methods share a name, later methods receive previous methods as their last parameter.
*
* @template S The type of the subject/state object.
* @param subject The state object to bind methods to.
* @returns A curried function that takes method objects and composes overlapping method names.
*
* @example
* // Automatic composition of save methods
* const api = makeWithCompose({ data: [] })({
* save: (s, item) => ({ data: [...s.data, item] }),
* save: (s, item, prevSave) => {
* console.log('Saving item:', item);
* const result = prevSave(s, item);
* console.log('Item saved successfully');
* return result;
* }
* });
*
* @example
* // Composing with multiple method objects
* const userAPI = makeWithCompose({ users: [] })(
* { save: (s, user) => ({ users: [...s.users, user] }) },
* { save: (s, user, prevSave) => prevSave(s, { ...user, timestamp: Date.now() }) },
* { save: (s, user, prevSave) => {
* if (!user.email) throw new Error('Email required');
* return prevSave(s, user);
* }
* }
* );
*/
export declare function makeWithCompose<S extends object>(subject: S): (...methodObjects: Methods<S>[]) => ChainableApi<any, S>;
/**
* Composes two factory functions where the second depends on the result of the first,
* merging their results into a single object. This is a powerful, general-purpose
* utility for building complex objects from dependent parts.
*
* @template P - A function that creates the primary object.
* @template S - A function that takes the result of `P` and creates a secondary object.
* @param primaryFactory The first function to execute.
* @param secondaryFactory The second function, which receives the object returned by `primaryFactory`.
* @returns A new, single factory function that runs the entire sequence.
*
* @example
* const createUser = (name: string) => ({ name, id: Math.random() });
* const addPermissions = (user: { id: number }) => ({
* permissions: user.id > 0.5 ? ['admin'] : ['user']
* });
*
* const createFullUser = enrich(createUser, addPermissions);
* const user = createFullUser('Alice');
* // { name: 'Alice', id: 0.7, permissions: ['admin'] }
*
* @example
* // Building a configuration object
* const createBaseConfig = (env: string) => ({
* environment: env,
* debug: env === 'development'
* });
* const addDatabaseConfig = (config: { environment: string }) => ({
* database: {
* host: config.environment === 'production' ? 'prod.db' : 'dev.db',
* ssl: config.environment === 'production'
* }
* });
*
* const createConfig = enrich(createBaseConfig, addDatabaseConfig);
* const config = createConfig('production');
* // { environment: 'production', debug: false, database: { host: 'prod.db', ssl: true } }
*/
export declare function enrich<P extends (...args: any[]) => object, S extends (primaryResult: ReturnType<P>) => object>(primaryFactory: P, secondaryFactory: S): (...args: Parameters<P>) => ReturnType<P> & ReturnType<S>;
/**
* Creates a complex, multi-layered API using a fluent, curried interface.
* Each subsequent layer can be either:
* 1. An object of methods to bind to the current API
* 2. A function that receives the current API and returns methods to bind
*
* Function layers enable dynamic layer creation based on the current state of the API.
* Each layer is "self-aware" and receives the instance from previous layers as its context,
* enabling powerful orchestration and composition.
*
* The chain is terminated by calling the final returned function with no arguments.
*
* @template S The type of the initial state or configuration object.
* @param subject The initial state object.
* @returns A curried function that takes the base methods and begins the layering process.
*
* @example
* // Basic usage with object layers
* const basicCounter = makeLayered({ count: 1 })
* (makeChainable({ add: (s, n) => ({ ...s, count: s.count + n }) })) // Base
* ({ get: (s) => s.count }) // Getters
* ({ double: (self) => self.add(self.get()) }) // Enhancers
* (); // Finalize
*
* @example
* // Advanced usage with function layers
* const advancedCounter = makeLayered({ count: 1, multiplier: 2 })
* // Base chainable layer
* (makeChainable({
* add: (s, n: number) => ({ ...s, count: s.count + n }),
* multiply: (s) => ({ ...s, count: s.count * s.multiplier })
* }))
* // Object layer - simple getters
* ({
* get: (s) => s.count,
* getMultiplier: (s) => s.multiplier
* })
* // Function layer - receives the current API and creates methods that use it
* ((api) => ({
* addAndGet: (s, n: number) => {
* const newApi = api.add(n);
* return newApi.get();
* },
* smartIncrement: (s) => {
* const current = api.get();
* return current < 10 ? api.add(1).get() : api.add(current * 0.1).get();
* }
* }))
* // Another function layer building on previous layers
* ((api) => ({
* performComplexOperation: (s, input: number) => {
* const doubled = api.multiply().get();
* const added = api.addAndGet(input);
* return doubled + added;
* }
* }))
* (); // Finalize the API
*/
export declare function makeLayered<S extends object>(subject: S): <BaseFns extends Methods<S>>(baseFns: BaseFns) => LayeredApiBuilder<ChainableApi<BaseFns, S>>;
/**
* Alias for `_with`. Provides a more semantic name for partial application scenarios.
*
* @example
* const config = { apiKey: 'abc123', timeout: 5000 };
* const [get, post] = provide(config)(
* (cfg, url) => `GET ${url} with key ${cfg.apiKey}`,
* (cfg, url, data) => `POST ${url} with ${JSON.stringify(data)}`
* );
*/
export declare const provide: typeof _with;
/**
* Alias for `make`. Emphasizes the collection aspect when gathering functions.
*
* @example
* function validateEmail(email) { return email.includes('@'); }
* function validatePassword(pwd) { return pwd.length >= 8; }
*
* const validators = collectFns(validateEmail, validatePassword);
* // { validateEmail: [Function], validatePassword: [Function] }
*/
export declare const collectFns: typeof make;
/**
* Alias for `makeWith`. Emphasizes the "providing to" relationship.
*
* @example
* const state = { items: [], loading: false };
* const api = provideTo(state)({
* getItems: (s) => s.items,
* isLoading: (s) => s.loading
* });
*/
export declare const provideTo: typeof makeWith;
/**
* Alias for `rebind`. Uses more intuitive "chainable" terminology.
*
* @example
* const chainableOps = makeChainable({
* push: (arr, item) => [...arr, item],
* filter: (arr, predicate) => arr.filter(predicate),
* map: (arr, fn) => arr.map(fn)
* });
*
* const listAPI = makeWith([1, 2, 3])(chainableOps);
* const result = listAPI.push(4).filter(x => x > 2).map(x => x * 2);
*/
export declare const makeChainable: typeof rebind;
/**
* Default export containing all functions with their primary names.
* Useful for importing the entire library or for environments that prefer default imports.
*
* @example
* import FL from './functional-library';
*
* const api = FL.makeWith(state)(FL.makeChainable(methods));
*
* @example
* import { makeWith, makeChainable, makeLayered } from './functional-library';
*
* const layeredAPI = makeLayered(initialState)
* (makeChainable(baseMethods))
* (additionalMethods)
* ();
*/
declare const _default: {
with: typeof _with;
provide: typeof _with;
make: typeof make;
collectFns: typeof make;
makeWith: typeof makeWith;
makeWithCompose: typeof makeWithCompose;
provideTo: typeof makeWith;
rebind: typeof rebind;
makeChainable: typeof rebind;
enrich: typeof enrich;
makeLayered: typeof makeLayered;
compose: typeof compose;
merge: typeof merge;
createMerger: typeof createMerger;
getKeyDescriptions: typeof getKeyDescriptions;
createProxy: typeof createProxy;
getSet: typeof getSet;
ignoreCase: typeof ignoreCase;
noSpecialChars: typeof noSpecialChars;
fallback: typeof fallback;
createLens: typeof createLens;
withFallback: typeof withFallback;
};
export default _default;
/** Type guard to check if a value is a valid LayerFunction */
export type IsLayerFunction<T> = T extends (api: any) => Methods<any> ? true : false;
/**
* A method that can compose with a previous method of the same name.
* The previous method is provided as the last parameter.
*/
export type ComposableMethod<S, Args extends any[], R, PrevR = R> = (subject: S, ...args: [...Args, (subject: S, ...args: Args) => PrevR]) => R;
/**
* A collection of composable methods where each can access the previous method.
*/
export type ComposableMethods<S extends object, PrevMethods extends Methods<S> = {}> = {
[K in keyof PrevMethods]?: PrevMethods[K] extends (s: S, ...args: infer A) => infer R ? ComposableMethod<S, A, R, R> : never;
};
//# sourceMappingURL=index.d.ts.map