UNPKG

@doeixd/make-with

Version:

Lightweight function application utilities

1,080 lines 42.1 kB
/** * @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