Skip to main content

Partial Application & Currying

Master default values and function transformation techniques for cleaner, more reusable code.

Partial Application

Partial application means pre-filling some properties with default values.

Basic Usage

import { partial } from '@noony-serverless/type-builder';

interface User {
id: number;
name: string;
role: 'admin' | 'user' | 'guest';
active: boolean;
age: number;
}

// Define defaults
const defaultUser = partial<User>({
role: 'user',
active: true,
age: 18,
});

// Use in pipeline
const user = userBuilder.build(
pipe<User>(
defaultUser, // Apply defaults first
userBuilder.withId(1),
userBuilder.withName('Alice')
)(userBuilder.empty())
);

// Result: { id: 1, name: 'Alice', role: 'user', active: true, age: 18 }

Why Use Partial Application?

1. DRY (Don't Repeat Yourself)

// ❌ Without partial (repetitive)
const admin1 = pipe(
userBuilder.withRole('admin'),
userBuilder.withActive(true),
userBuilder.withId(1)
);

const admin2 = pipe(
userBuilder.withRole('admin'),
userBuilder.withActive(true),
userBuilder.withId(2)
);

// ✅ With partial (reusable)
const adminDefaults = partial<User>({
role: 'admin',
active: true,
});

const admin1 = pipe(adminDefaults, userBuilder.withId(1));
const admin2 = pipe(adminDefaults, userBuilder.withId(2));

2. Configuration Management

// Development defaults
const devDefaults = partial<Config>({
env: 'development',
debug: true,
logLevel: 'verbose',
});

// Production defaults
const prodDefaults = partial<Config>({
env: 'production',
debug: false,
logLevel: 'error',
});

const devConfig = configBuilder.build(
pipe(devDefaults, configBuilder.withPort(3000))(configBuilder.empty())
);

3. Template Objects

// Create templates for common user types
const templates = {
admin: partial<User>({ role: 'admin', active: true }),
guest: partial<User>({ role: 'guest', active: false }),
moderator: partial<User>({ role: 'user', active: true }),
};

const admin = userBuilder.build(pipe(templates.admin, userBuilder.withId(1))(userBuilder.empty()));

Partial Variants

partialDefaults - Non-Overwriting

Apply defaults only if property doesn't exist:

import { partialDefaults } from '@noony-serverless/type-builder';

const defaults = partialDefaults<User>({
age: 18,
active: true,
});

// Property doesn't exist → use default
const state1 = defaults({ name: 'Alice' });
// { name: 'Alice', age: 18, active: true }

// Property exists → keep existing value
const state2 = defaults({ name: 'Bob', age: 30 });
// { name: 'Bob', age: 30, active: true } ← age NOT overwritten

Use case: Safe defaults that don't overwrite user input.

partialOverwrite - Always Overwrite

Apply values even if property exists:

import { partialOverwrite } from '@noony-serverless/type-builder';

const forceInactive = partialOverwrite<User>({
active: false,
});

const state = forceInactive({ id: 1, name: 'Alice', active: true });
// { id: 1, name: 'Alice', active: false } ← active overwritten

Use case: Forced updates, data sanitization.

partialTemplates - Multiple Named Templates

Define multiple templates in one object:

import { partialTemplates } from '@noony-serverless/type-builder';

const userTemplates = partialTemplates<User>({
admin: { role: 'admin', active: true },
guest: { role: 'guest', active: false },
moderator: { role: 'user', active: true },
});

// Use templates
const admin = userBuilder.build(
pipe(
userTemplates.admin,
userBuilder.withId(1),
userBuilder.withName('Admin')
)(userBuilder.empty())
);

const guest = userBuilder.build(
pipe(
userTemplates.guest,
userBuilder.withId(2),
userBuilder.withName('Guest')
)(userBuilder.empty())
);

Use case: Organized defaults for different scenarios.

partialIf - Conditional Defaults

Apply defaults only if condition is true:

import { partialIf } from '@noony-serverless/type-builder';

const applyDefaults = partialIf<User>(
(state) => !state.role, // Condition: no role set
{ role: 'user', active: true } // Defaults to apply
);

const state1 = applyDefaults({ id: 1, name: 'Alice' });
// { id: 1, name: 'Alice', role: 'user', active: true } ← defaults applied

const state2 = applyDefaults({ id: 2, name: 'Bob', role: 'admin' });
// { id: 2, name: 'Bob', role: 'admin' } ← defaults NOT applied

Use case: Smart defaults based on state.


Currying

Currying transforms a multi-argument function into a chain of single-argument functions.

What Is Currying?

// Regular function
const add = (a: number, b: number) => a + b;
add(5, 3); // 8

// Curried version
const curriedAdd = curry2(add);
const add5 = curriedAdd(5); // Returns function
add5(3); // 8
add5(10); // 15

Why Curry?

1. Partial Application

const multiply = (a: number, b: number) => a * b;
const curriedMultiply = curry2(multiply);

const double = curriedMultiply(2);
const triple = curriedMultiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

2. Function Composition

const setField = curry3(
<T, K extends keyof T>(key: K, value: T[K], state: BuilderState<T>) =>
({ ...state, [key]: value }) as BuilderState<T>
);

const setName = setField('name');
const setNameAlice = setName('Alice');

const state = setNameAlice({}); // { name: 'Alice' }

3. Reusable Transformations

const validateField = curry2(<T>(validator: (v: any) => boolean, state: BuilderState<T>) => {
// Validation logic
return state;
});

const isEmail = (v: string) => /\S+@\S+\.\S+/.test(v);
const isAdult = (v: number) => v >= 18;

const validateEmail = validateField(isEmail);
const validateAge = validateField(isAdult);

// Reuse validators
const validUser1 = validateEmail(user1);
const validUser2 = validateAge(user2);

Curry Helpers

curry2 - Two Arguments

import { curry2 } from '@noony-serverless/type-builder';

const add = (a: number, b: number) => a + b;
const curriedAdd = curry2(add);

curriedAdd(5)(3); // 8

// Or partially apply
const add5 = curriedAdd(5);
add5(3); // 8
add5(10); // 15

curry3 - Three Arguments

import { curry3 } from '@noony-serverless/type-builder';

const sum3 = (a: number, b: number, c: number) => a + b + c;
const curriedSum = curry3(sum3);

curriedSum(1)(2)(3); // 6

// Or partially apply
const add1 = curriedSum(1);
const add1and2 = add1(2);
add1and2(3); // 6

curry4 - Four Arguments

import { curry4 } from '@noony-serverless/type-builder';

const sum4 = (a: number, b: number, c: number, d: number) => a + b + c + d;
const curriedSum = curry4(sum4);

curriedSum(1)(2)(3)(4); // 10

autoCurry - Auto-Detect Arity

import { autoCurry } from '@noony-serverless/type-builder';

const add = (a: number, b: number) => a + b;
const curriedAdd = autoCurry(add); // Uses function.length

curriedAdd(5)(3); // 8

Note: autoCurry uses function.length, which doesn't work with rest parameters or default arguments.


Uncurrying

Convert curried functions back to regular functions:

import { uncurry2 } from '@noony-serverless/type-builder';

const curriedAdd = (a: number) => (b: number) => a + b;
const regularAdd = uncurry2(curriedAdd);

regularAdd(5, 3); // 8

Flip

Reverse argument order:

import { flip, curry2 } from '@noony-serverless/type-builder';

const divide = (a: number, b: number) => a / b;
const curriedDivide = curry2(divide);
const flippedDivide = flip(curriedDivide);

divide(10, 2); // 5
flippedDivide(2)(10); // 5 (arguments flipped)

Use case: Adapting function signatures for composition.


Real-World Examples

Example 1: Form Validators

// Define reusable validators
const validateMin = curry2((min: number, value: number) => value >= min);

const validateMax = curry2((max: number, value: number) => value <= max);

const validateRange = curry3(
(min: number, max: number, value: number) => value >= min && value <= max
);

// Create specific validators
const isAdult = validateMin(18);
const isSenior = validateMin(65);
const isChild = validateMax(12);
const isWorkingAge = validateRange(18, 65);

// Use in validation
if (!isAdult(user.age)) {
throw new Error('Must be 18 or older');
}

Example 2: Data Transformation Pipeline

// Curried transformers
const normalizeField = curry2(<T, K extends keyof T>(key: K, state: BuilderState<T>) => {
const value = state[key];
if (typeof value === 'string') {
return { ...state, [key]: value.trim().toLowerCase() } as BuilderState<T>;
}
return state;
});

const validateField = curry3(
<T, K extends keyof T>(key: K, validator: (v: T[K]) => boolean, state: BuilderState<T>) => {
const value = state[key];
if (value && !validator(value)) {
throw new Error(`Invalid ${String(key)}`);
}
return state;
}
);

// Create specific transformations
const normalizeEmail = normalizeField('email');
const normalizeName = normalizeField('name');
const validateEmail = validateField('email', (v: string) => v.includes('@'));
const validateAge = validateField('age', (v: number) => v >= 0);

// Compose into pipeline
const processUser = pipe<User>(normalizeEmail, normalizeName, validateEmail, validateAge);

Example 3: Configuration Builder

// Curried config setters
const setEnv = curry2((env: string, config: Config) => ({ ...config, env }));

const setDebug = curry2((debug: boolean, config: Config) => ({ ...config, debug }));

const setPort = curry2((port: number, config: Config) => ({ ...config, port }));

// Create environment-specific configs
const toDevelopment = pipe(setEnv('development'), setDebug(true), setPort(3000));

const toProduction = pipe(setEnv('production'), setDebug(false), setPort(80));

const devConfig = toDevelopment(baseConfig);
const prodConfig = toProduction(baseConfig);

Combining Partial and Currying

Partial application and currying work great together:

// Curried factory function
const createUser = curry3((defaults: Partial<User>, id: number, name: string) =>
userBuilder.build(
pipe(partial(defaults), userBuilder.withId(id), userBuilder.withName(name))(userBuilder.empty())
)
);

// Create specialized factories
const createAdmin = createUser({ role: 'admin', active: true });
const createGuest = createUser({ role: 'guest', active: false });

// Use factories
const admin1 = createAdmin(1, 'Admin User');
const admin2 = createAdmin(2, 'Another Admin');
const guest1 = createGuest(3, 'Guest User');

Performance Tips

1. Curry Outside Loops

// ❌ Bad - curries inside loop
users.map((user) => curry2(transform)(user.id)(user));

// ✅ Good - curry once
const curriedTransform = curry2(transform);
users.map((user) => curriedTransform(user.id)(user));

2. Reuse Partial Applications

// ❌ Bad - creates new partial every time
function processUsers(users: User[]) {
return users.map((u) =>
pipe(partial({ active: true }), userBuilder.withId(u.id))(userBuilder.empty())
);
}

// ✅ Good - create partial once
const activeDefaults = partial<User>({ active: true });
function processUsers(users: User[]) {
return users.map((u) => pipe(activeDefaults, userBuilder.withId(u.id))(userBuilder.empty()));
}

Summary

Partial Application

FunctionBehaviorUse Case
partialMerge defaultsBasic defaults
partialDefaultsApply only if missingSafe defaults
partialOverwriteAlways overwriteForced updates
partialTemplatesNamed templatesOrganized configs
partialIfConditional defaultsSmart defaults

Currying

FunctionPurposeExample
curry2Curry 2-arg functioncurry2((a, b) => a + b)
curry3Curry 3-arg functioncurry3((a, b, c) => a + b + c)
curry4Curry 4-arg functioncurry4((a, b, c, d) => ...)
autoCurryAuto-detect arityautoCurry(fn)
uncurry2Uncurry functionuncurry2(curried)
flipReverse argumentsflip(curried)

Quick Reference

// Partial application
const defaults = partial<T>({ key: value });
const user = pipe(defaults, ...)(empty());

// Currying
const curried = curry2((a, b) => a + b);
const add5 = curried(5);
add5(3); // 8

Next Steps