A TypeScript library that brings named arguments and robust, type-safe partial application to JavaScript/TypeScript functions. The library features enhanced type safety with compile-time checking against parameter duplication and precise return type inference based on parameter requirements.
- Type-Safe Partial Application: Prevents reapplying the same parameter multiple times
- Precise Return Types: TypeScript distinguishes between partial and complete application
- Parameter Tracking: Maintains type safety across multiple partial applications
- Smart Builder Pattern: Track which parameters have been applied during building
- Object Parameter Updates: Safely update previously applied object parameters with reApply
- Nested Property Access: Access deeply nested properties with dot notation
- Composability Utilities: Transform, group, validate, combine, and pipeline arguments
npm install @doeixd/named-args
- Installation
- Core Concepts
- Named Arguments
- Partial Application
- Builder Pattern
- Nested Property Access
- Object Property Arguments
- Composability Utilities
- Advanced Use Cases
- Configurable Functions
- Advanced Features
- Why This Matters
- Gotchas
- Example: Building a Chart API
- API Reference
- License
Named arguments are "branded" with metadata that allows the library to track which parameter they represent. This branding is what enables calling functions with arguments in any order.
// Under the hood, each named argument is branded with its parameter name
const emailArg = args.email('[email protected]');
// Represents: { [BRAND_SYMBOL]: { name: 'email', value: 'john@example.com' } }
// This allows calling functions with arguments in any order
namedCreateUser(
args.email('[email protected]'),
args.firstName('John'),
// TypeScript knows which parameter each argument represents
);
The library transforms regular functions into ones that can accept named arguments through a process that:
- Analyzes the function's parameter structure
- Creates branded argument accessors for each parameter
- Returns a new function that can map named arguments back to positional arguments
// Original function
function sendEmail(to: string, subject: string, body: string) {
// Implementation
}
// Transform into a function accepting named arguments
const [args, namedSendEmail] = createNamedArguments(sendEmail);
// Now we can call it with named arguments in any order
namedSendEmail(
args.subject('Meeting reminder'),
args.to('[email protected]'),
args.body('Don\'t forget our meeting tomorrow.')
);
Unlike traditional currying which requires parameters in a specific order, this library enables:
- Applying any subset of arguments in any order
- Creating multiple layers of partial application
- Maintaining full type safety throughout the process
// Create named arguments for a function
function formatNumber(value: number, locale: string, style: string, currency?: string) {
return new Intl.NumberFormat(locale, { style, currency }).format(value);
}
const [args, namedFormat] = createNamedArguments(formatNumber);
// Create a partial application - note any subset of args can be applied
const formatUSD = namedFormat.partial(
args.style('currency'),
args.currency('USD')
);
// Create another layer of specialization
const formatUSPrice = formatUSD.partial(args.locale('en-US'));
// Finally apply the remaining argument
console.log(formatUSPrice(args.value(42.99))); // "$42.99"
The configurability pattern extends partial application by separating:
- What is being configured (which parameters)
- How they're being configured (the values)
- When they're being applied (the execution)
This creates a powerful API design pattern that promotes reusability and composition.
// Create a configurable function
const configureFetch = createConfigurableFunction([args, namedFetch]);
// Define a configuration for JSON API requests
const jsonRequest = configureFetch(args => {
args.headers({
'Accept': 'application/json',
'Content-Type': 'application/json'
});
});
// Use the configured function with remaining parameters
const response = await jsonRequest(
args.url('https://api.example.com/users'),
args.method('GET')
);
import { createNamedArguments } from '@doeixd/named-args';
// A function with several parameters
function createUser(firstName: string, lastName: string, age: number, email: string) {
return { firstName, lastName, age, email };
}
// Create named arguments for the function
// The type parameter specifies the argument names matching the function parameters
const [args, namedCreateUser] = createNamedArguments<
typeof createUser,
{firstName: string, lastName: string, age: number, email: string}
>(createUser);
// Use named arguments in any order
const user = namedCreateUser(
args.email('[email protected]'),
args.firstName('John'),
args.age(30),
args.lastName('Doe')
);
console.log(user);
// { firstName: 'John', lastName: 'Doe', age: 30, email: 'john.doe@example.com' }
The library provides precise return type inference based on parameter requirements:
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}!`;
}
const [args, namedGreet] = createNamedArguments<
typeof greet,
{ name: string; greeting?: string }
>(
greet,
[
{ name: "name", required: true },
{ name: "greeting", required: false }
]
);
// TypeScript knows this returns a string (not a function)
// because all required parameters are provided
const greeting = namedGreet(args.name("World")); // Type: string
// TypeScript knows this returns a partially applied function
// because no required parameters are provided yet
const partialGreet = namedGreet.partial(); // Type: BrandedFunction<...>
This makes it easier to work with partially applied functions, as you no longer need to manually check whether the result is a value or a function.
The library provides enhanced type-safety for partial application:
import { createNamedArguments } from "@doeixd/named-args";
function add(a: number, b: number, c: number): number {
return a + b + c;
}
// Create named arguments with type information
const [args, namedAdd] = createNamedArguments<
typeof add,
{ a: number; b: number; c: number }
>(add);
// Create a partial application with "a"
const addWithA = namedAdd.partial(args.a(5));
// TypeScript prevents you from applying "a" again
// This would cause a compile-time error:
// const error = addWithA(args.a(10)); // Error: Parameter "a" already applied
// You can apply other parameters
const addWithAB = addWithA.partial(args.b(10));
// Complete the application
const result = addWithAB(args.c(15)); // 30
Unlike other partial application libraries, this one maintains full type-safety during each step, making it impossible to accidentally provide the same parameter multiple times.
import { createNamedArguments } from '@doeixd/named-args';
function formatCurrency(amount: number, currency: string, locale: string) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
// Create named arguments
const [args, namedFormat] = createNamedArguments(formatCurrency);
// Create a partial application for USD in US English
const formatUSD = namedFormat(
args.currency('USD'),
args.locale('en-US')
);
// Use the partial application with remaining arguments
const price = formatUSD(args.amount(1234.56));
console.log(price); // "$1,234.56"
Unlike traditional currying which requires parameters in a specific order, this approach lets you apply arguments in any order, at any time.
You can create multiple layers of specialization, building on previous partial applications:
// First stage: Create base API request with common headers
const apiRequest = namedRequest(
args.headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-Key': 'your-api-key'
})
);
// Second stage: Create method-specific requests
const getRequest = apiRequest(args.method('GET'));
const postRequest = apiRequest(args.method('POST'));
// Third stage: Domain-specific requests
const userApiGet = getRequest(
args.url('https://api.example.com/users'),
args.timeout(5000)
);
This allows for a tree of increasingly specialized functions that builds naturally as needed.
The library provides the reApply
method, which allows you to safely update previously applied object parameters without reapplying the entire parameter:
// Start with a base client configuration
const baseClient = namedRequest.partial(
args.method('POST'),
args.options({
headers: {
contentType: 'application/json',
accept: 'application/json'
}
}),
args.logOptions({
level: 'info',
format: 'json'
})
);
// Add authentication by updating only the headers property
const authClient = baseClient.reApply(args.options, (prev) => ({
...prev,
headers: {
...prev.headers,
authorization: 'Bearer token123'
}
}));
// Add retry logic to the same options object
const retryClient = authClient.reApply(args.options, (prev) => ({
...prev,
retries: {
count: 3,
delay: 1000
},
cache: false
}));
// Update logging options separately
const debugClient = retryClient.reApply(args.logOptions, (prev) => ({
...prev,
level: 'debug',
destination: 'console'
}));
// Make the final request with all accumulated options
const result = debugClient(args.url('https://api.example.com/data'));
The reApply
method:
- Takes the name of a previously applied parameter
- Accepts an updater function that receives the current value and returns a new value
- Maintains type safety, only allowing updates to parameters that have been applied
- Returns a new branded function with the updated parameter value
The library provides a builder pattern that maintains type-safety:
import { createNamedArguments, createBuilder } from "@doeixd/named-args";
function configureApp(port: number, host: string, database: DbConfig, logging?: boolean) {
// Create app configuration
return { port, host, database, logging };
}
const [args, namedConfig] = createNamedArguments(configureApp);
// Create a builder
const appBuilder = createBuilder(namedConfig);
// Use the builder pattern to construct the configuration
// The builder tracks which parameters have been applied and prevents duplicates
const devConfig = appBuilder
.with(args.port(3000))
.with(args.host("localhost"))
.with(args.database({ url: "localhost:27017", name: "devdb" }))
.execute();
// Attempting to set the same parameter twice would
// result in both compile-time errors and runtime warnings
The builder pattern is particularly useful for creating complex objects with many parameters, while maintaining full type-safety.
The library now provides a specialized primitive for working with deeply nested object properties:
import { createNamedArguments, createNestedArgs } from '@doeixd/named-args';
// Function with a complex nested configuration object
function setupApplication(config: {
server: {
port: number;
host: string;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
credentials: {
username: string;
password: string;
};
};
}) {
// Implementation
}
// Create named arguments
const [args, namedSetup] = createNamedArguments(setupApplication);
// Create nested arguments for the config parameter
type ConfigType = Parameters<typeof setupApplication>[0];
const config = createNestedArgs<ConfigType>('config');
// Use the nested arguments with convenient dot notation
const app = namedSetup(
config.server.port(8080),
config.server.ssl.enabled(true),
config.database.credentials.username('admin')
);
This approach provides several benefits:
- Full TypeScript type safety at any nesting depth
- Intuitive dot notation for accessing nested properties
- Seamless integration with partial application
- No need to manually construct property paths
This provides a simpler alternative to createNestedArgs
when you only need to access top-level properties of an object parameter:
import { createNamedArguments, createObjectPropertyArgs } from '@doeixd/named-args';
// Function with an options object parameter
function configureServer(options: {
port: number;
host: string;
ssl: boolean;
maxConnections: number;
}) {
// Implementation
}
// Create named arguments for the function
const [args, namedConfig] = createNamedArguments(configureServer);
// Create property-level named args for the options object
type OptionsType = Parameters<typeof configureServer>[0];
const optionArgs = createObjectPropertyArgs<OptionsType>('options');
// Use the property-level named args
const server = namedConfig(
optionArgs.port(8080),
optionArgs.host('localhost'),
optionArgs.ssl(true),
optionArgs.maxConnections(100)
);
Benefits compared to createNestedArgs
:
- Simpler implementation with less overhead
- Focused on the common case of accessing top-level properties
- Provides the same type safety for first-level properties
- Works seamlessly with partial application and other library features# Named Arguments
The library now includes utilities for transforming and combining arguments in powerful ways:
import { transformArg } from '@doeixd/named-args/composability';
// Create a transformer that converts string dates to Date objects
const timestampArg = transformArg(args.timestamp, (isoString: string) => new Date(isoString));
// Now we can pass strings instead of Date objects
const result = namedProcess(
timestampArg('2023-01-15T12:30:00Z'),
args.value(42)
);
import { createArgGroup } from '@doeixd/named-args/composability';
// Create an argument group for connection parameters
const connectionGroup = createArgGroup({
host: args.host,
port: args.port,
credentials: createArgGroup({
username: args.credentials.username,
password: args.credentials.password
})
});
// Apply all connection settings with one object
const db = namedConnect(
...connectionGroup({
host: 'localhost',
port: 5432,
credentials: {
username: 'admin',
password: 'secret123'
}
})
);
import { pipeline } from '@doeixd/named-args/composability';
// Create a pipeline for processing amounts
const amountPipeline = pipeline(args.amount)
.map((value: string) => parseFloat(value))
.map(value => Math.abs(value))
.map(value => Math.round(value * 100) / 100);
// Process a value through the pipeline
const transaction = namedProcess(
amountPipeline("42.567"), // Converts to 42.57
args.timestamp(new Date()),
args.description("Office supplies")
);
import { combineArgs } from '@doeixd/named-args/composability';
// Calculate area automatically from width and height
const autoArea = combineArgs(
args.area,
([width, height]) => width * height,
args.width,
args.height
);
// Apply the combined argument
const rectangle = namedCalculate(
args.width(5),
args.height(10),
...autoArea() // Automatically sets area = 50
);
import { withDefault } from '@doeixd/named-args/composability';
// Create a default value for the greeting
const defaultGreeting = withDefault(args.greeting, "Hi");
// Use it when you want the default value
const result = namedGreet(
args.name("World"),
...defaultGreeting() // Uses "Hi" as the default
);
import { withValidation } from '@doeixd/named-args/composability';
// Create validated arguments
const validatedAmount = withValidation(
args.amount,
(value) => value > 0 && value <= 10000,
"Amount must be between 0 and 10,000"
);
// Will throw an error if validation fails
const transfer = namedTransfer(
validatedAmount(500),
args.accountId("ACC1234567890")
);
The enhanced type system enables safer function composition patterns:
// Create a pipeline of transformations with type-safe partial application
const processData = pipe(
fetchData.partial(args.endpoint("/api/users")),
filterData.partial(args.predicate(user => user.active)),
sortData.partial(args.key("lastName")),
paginateData.partial(args.pageSize(10))
);
// Each step maintains type safety and prevents parameter duplication
const results = processData(args.page(2));
Create flexible service configurations with partial application:
// Define a service that requires multiple dependencies
function createUserService(db: Database, logger: Logger, cache: Cache) {
return {
findUser: (id: string) => { /* ... */ },
createUser: (data: UserData) => { /* ... */ }
};
}
const [args, namedService] = createNamedArguments(createUserService);
// Create partially configured services for different environments
const testService = namedService.partial(
args.db(testDb),
args.logger(mockLogger)
);
const prodService = namedService.partial(
args.db(prodDb),
args.logger(prodLogger)
);
// Later, complete the configuration
const localTestService = testService(args.cache(localCache));
const remoteTestService = testService(args.cache(redisCache));
import { createNamedArguments, createConfigurableFunction } from '@doeixd/named-args';
function processArray<T>(
array: T[],
filterFn: (item: T) => boolean,
sortFn?: (a: T, b: T) => number,
limit?: number
): T[] {
let result = array.filter(filterFn);
if (sortFn) {
result = result.sort(sortFn);
}
if (limit !== undefined && limit >= 0) {
result = result.slice(0, limit);
}
return result;
}
// Create named arguments with explicit parameter names that match the function
const [args, namedProcess] = createNamedArguments<
typeof processArray,
{array: T[], filterFn: (item: T) => boolean, sortFn?: (a: T, b: T) => number, limit?: number}
>(processArray);
// Create a configurable function
const configureArrayProcessor = createConfigurableFunction([args, namedProcess]);
// Configure a processor for top N positive numbers
const topPositiveNumbers = configureArrayProcessor(args => {
// Filter for positive numbers
args.filterFn(num => num > 0);
// Sort in descending order
args.sortFn((a, b) => b - a);
});
// The resulting function accepts the remaining parameters
const numbers = [-5, 10, 3, -2, 8, 1, -1, 6];
const top3Positive = topPositiveNumbers(args.array(numbers), args.limit(3));
console.log(top3Positive); // [10, 8, 6]
The library supports rest parameters:
function sum(first: number, ...rest: number[]) {
return [first, ...rest].reduce((a, b) => a + b, 0);
}
const [args, namedSum] = createNamedArguments(sum);
console.log(namedSum(args.first(1), args.rest(2, 3, 4))); // 10
Default parameter values are respected:
function greet(name: string, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
const [args, namedGreet] = createNamedArguments(greet);
console.log(namedGreet(args.name("World"))); // "Hello, World!"
console.log(namedGreet(args.name("Friend"), args.greeting("Hi"))); // "Hi, Friend!"
These patterns provide several key benefits:
- Composability: Functions can be specialized incrementally
- Reusability: Partially applied functions create reusable building blocks
- Separation of Concerns: Configure different aspects of a function independently
- Type Safety: Maintain full TypeScript type checking at every stage
- Readability: Self-documenting code that clearly shows which arguments are being used
- Flexibility: Work with complex nested structures in an intuitive way
- Transformability: Process and validate arguments with pipelines and transformers
This library takes the functional programming concept of partial application and makes it more practical and flexible for real-world TypeScript applications, enabling elegant API designs that would be cumbersome with traditional approaches.
When creating named arguments, explicitly providing type parameters improves inference:
// May have incomplete inference without type parameters
const [args, namedFn] = createNamedArguments(myFunction);
// Better to be explicit for complex functions
const [args, namedFn] = createNamedArguments<
typeof myFunction,
{param1: string, param2: number}
>(myFunction);
The library may struggle with complex function overloads. Specify a single overload signature when creating named arguments:
// For overloaded functions, specify which overload to use
const [args, namedFetch] = createNamedArguments<
(url: string, options?: RequestInit) => Promise<Response>,
{url: string, options?: RequestInit}
>(fetch);
Named arguments add a small overhead compared to direct function calls:
- Each argument is wrapped in a branded object
- The function performs argument matching at runtime
- Consider using direct calls in performance-critical paths
// Traditional approach with a charting library
function createTimeSeriesChart(element, data, options = {}) {
// Merge user options with defaults
const config = {
type: 'line',
xAxis: { key: 'timestamp', label: 'Time' },
animation: { enabled: true, duration: 800 },
tooltip: { enabled: true },
...options
};
return createChart(element, config);
}
// Complex, nested, error-prone configuration
const tempChart = createTimeSeriesChart(
document.getElementById('chart'),
temperatureData,
{
yAxis: { key: 'value', label: 'Temperature (°F)', min: 0 },
// Oops, typo in property name won't be caught at compile time
animaton: { duration: 500 }
}
);
import { createNamedArguments, createConfigurableFunction } from '@doeixd/named-args';
// Define chart rendering with named parameters
function renderChart(
element: HTMLElement,
data: Array<Record<string, any>>,
type: 'bar' | 'line' | 'pie',
xAxis?: { key: string; label?: string },
yAxis?: { key: string; label?: string; min?: number },
animation?: { enabled?: boolean; duration?: number },
tooltip?: { enabled?: boolean }
) {
// Implementation
}
// Create named arguments
const [args, namedRenderChart] = createNamedArguments(renderChart);
// Create specialized chart builders
const configureChart = createConfigurableFunction([args, namedRenderChart]);
const createTimeSeriesChart = configureChart(args => {
args.type('line');
args.xAxis({ key: 'timestamp', label: 'Time' });
args.animation({ enabled: true, duration: 800 });
args.tooltip({ enabled: true });
});
// Type-safe usage with autocomplete and error checking
const tempChart = createTimeSeriesChart(
args.element(document.getElementById('chart')),
args.data(temperatureData),
args.yAxis({
key: 'value',
label: 'Temperature (°F)',
min: 0
})
// Typo would be caught by TypeScript:
// args.animaton({ duration: 500 }) ❌ Error!
);
This pattern makes your chart configuration:
- Type-safe: Errors caught at compile time
- Discoverable: IDE autocomplete shows available options
- Reusable: Create specialized chart builders with sensible defaults
- Clear: Arguments are explicitly named and can be applied in any order
Transforms a regular function into one that accepts named arguments.
Type Parameters:
F
: Type of the original functionA
: Record type describing the argument structure
Parameters:
func
: The function to transformparameters?
: Optional parameter metadata
Returns:
- A tuple containing:
- Named argument accessors (with properties matching the type
A
) - A branded function that accepts named arguments
- Named argument accessors (with properties matching the type
Creates named arguments for deeply nested object properties with full type safety.
Type Parameters:
T
: The type of the object whose nested properties will be accessed
Parameters:
basePath
: The base path for all properties (usually the parameter name)
Returns:
- A proxy object that provides type-safe access to all nested properties
Creates named arguments for individual properties of an object parameter.
Type Parameters:
T
: The object type whose properties will be accessed
Parameters:
paramName
: The name of the parameter in the function
Returns:
- An object with named arguments for each property of the object parameter
Creates a configurable function that can be pre-configured with specific arguments.
Parameters:
- A tuple containing named argument accessors and a branded function (from
createNamedArguments
)
Returns:
- A function that takes a setup function and returns a configured version of the original function
Creates a builder for constructing function calls with type-safe parameter tracking.
Parameters:
brandedFunc
: A branded function created withcreateNamedArguments
Returns:
- A builder instance with methods for adding arguments and executing the function
Creates a transformer for named arguments that applies a transformation function to values.
Parameters:
argCreator
: The original argument creator functiontransformer
: Function that transforms the input value to the required type
Returns:
- A new argument creator that accepts
U
and applies the transformation
Creates a group of related named arguments that can be applied together.
Parameters:
config
: Configuration mapping of property names to argument creators
Returns:
- Function that generates branded arguments from values
Creates a combined argument that merges multiple named arguments into one.
Parameters:
targetArg
: The argument to receive the combined valuecombiner
: Function that combines the source valuessourceArgs
: The source arguments to combine
Returns:
- Function that applies the sources and returns the combined argument
Provides a default value for an optional argument.
Parameters:
argCreator
: The argument creator functiondefaultValue
: The default value to use when the argument is not provided
Returns:
- Function that creates the argument with the default value
Creates a pipeline of transformations for a value before applying it as an argument.
Parameters:
argCreator
: The argument creator function for the final value
Returns:
- A pipeline builder object with methods:
map<V>(fn)
: Adds a transformation to the pipelinefilter(predicate, fallback)
: Adds a filter to the pipelineapply(value)
: Applies the pipeline to a value
Creates a wrapper function that provides validation for arguments.
Parameters:
argCreator
: The argument creator functionvalidator
: Function that validates the valueerrorMessage?
: Optional error message for validation failures
Returns:
- A new argument creator with validation
MIT