We use cookies to enhance your experience on the site
CodeWorlds

Functional Programming in JavaScript/TypeScript

Just as ancient mathematicians strove for elegant, pure mathematical solutions, functional programming focuses on pure functions, immutability, and composition. In this lesson, we will learn the fundamental concepts of functional programming and how to apply them in JavaScript/TypeScript to create more predictable, testable, and maintainable code.

Pure Functions

A pure function is a function that satisfies two conditions:

  1. Determinism - for the same arguments, it always returns the same result
  2. No side effects - it does not modify external state and does not perform I/O operations

Examples of impure functions

1// ❌ Impure function - depends on external state
2let tax = 0.23;
3
4function calculatePrice(basePrice) {
5  return basePrice * (1 + tax); // Depends on external variable
6}
7
8// ❌ Impure function - modifies external state
9let counter = 0;
10
11function incrementCounter() {
12  counter++; // Side effect - modification of global variable
13  return counter;
14}
15
16// ❌ Impure function - side effect (I/O)
17function greetUser(name) {
18  console.log(\`Hello, \${name}!\`); // Side effect - console output
19  return name;
20}
21
22// ❌ Impure function - nondeterminism
23function getCurrentTimestamp() {
24  return Date.now(); // Different results for each invocation
25}
26
27// ❌ Impure function - modifies argument
28function addItem(array, item) {
29  array.push(item); // Mutates the passed argument
30  return array;
31}

Examples of pure functions

1// ✅ Pure function - determinism and no side effects
2function calculatePrice(basePrice: number, taxRate: number): number {
3  return basePrice * (1 + taxRate);
4}
5
6// ✅ Pure function - mathematical operation
7function add(a: number, b: number): number {
8  return a + b;
9}
10
11// ✅ Pure function - data transformation
12function formatFullName(firstName: string, lastName: string): string {
13  return \`\${firstName} \${lastName}\`;
14}
15
16// ✅ Pure function - working with arrays (no mutation)
17function addItem<T>(array: T[], item: T): T[] {
18  return [...array, item]; // Returns a new array
19}
20
21// ✅ Pure function - filtering
22function filterEvenNumbers(numbers: number[]): number[] {
23  return numbers.filter(num => num % 2 === 0);
24}
25
26// ✅ Pure function - object transformation
27interface User {
28  id: number;
29  name: string;
30  age: number;
31}
32
33function updateUserAge(user: User, newAge: number): User {
34  return {
35    ...user, // Spread operator - copying existing properties
36    age: newAge // Overriding age
37  };
38}
39
40// ✅ Pure function - validation
41function isValidEmail(email: string): boolean {
42  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
43  return emailRegex.test(email);
44}
45
46// Using pure functions
47const products = [
48  { name: 'Laptop', basePrice: 2000 },
49  { name: 'Mouse', basePrice: 50 },
50  { name: 'Keyboard', basePrice: 150 }
51];
52
53const TAX_RATE = 0.23;
54
55const productsWithTax = products.map(product => ({
56  ...product,
57  finalPrice: calculatePrice(product.basePrice, TAX_RATE)
58}));
59
60console.log(productsWithTax);

Benefits of pure functions

1// Easy to test - no external dependencies
2function calculateDiscount(price: number, discountPercent: number): number {
3  return price * (discountPercent / 100);
4}
5
6// Unit test is simple and reliable
7console.assert(calculateDiscount(100, 10) === 10);
8console.assert(calculateDiscount(200, 25) === 50);
9
10// Memoization - optimization thanks to determinism
11function memoize<TArgs extends any[], TReturn>(
12  fn: (...args: TArgs) => TReturn
13): (...args: TArgs) => TReturn {
14  const cache = new Map<string, TReturn>();
15
16  return (...args: TArgs): TReturn => {
17    const key = JSON.stringify(args);
18
19    if (cache.has(key)) {
20      console.log('Cache hit!');
21      return cache.get(key)!;
22    }
23
24    console.log('Computing...');
25    const result = fn(...args);
26    cache.set(key, result);
27    return result;
28  };
29}
30
31// Computationally expensive function
32function fibonacci(n: number): number {
33  if (n <= 1) return n;
34  return fibonacci(n - 1) + fibonacci(n - 2);
35}
36
37// Memoized version
38const memoizedFibonacci = memoize(fibonacci);
39
40console.log(memoizedFibonacci(10)); // Computing... 55
41console.log(memoizedFibonacci(10)); // Cache hit! 55

Immutability

Immutability means that objects and data structures are not modified after creation. Instead, new versions are created with the applied changes.

Working with primitives

1// Primitives in JavaScript are immutable by nature
2let name = 'Jan';
3let newName = name.toUpperCase(); // Creates a new string
4console.log(name);    // 'Jan' - original unchanged
5console.log(newName); // 'JAN' - new value

Working with arrays

1// ❌ Mutable operations (modify the original array)
2const originalArray = [1, 2, 3];
3originalArray.push(4);        // Mutates
4originalArray.pop();          // Mutates
5originalArray.splice(1, 1);   // Mutates
6originalArray.sort();         // Mutates
7
8// ✅ Immutable operations (create new arrays)
9const numbers = [1, 2, 3];
10
11// Adding elements
12const withNewElement = [...numbers, 4];
13const withElementAtStart = [0, ...numbers];
14const withElementInMiddle = [...numbers.slice(0, 2), 2.5, ...numbers.slice(2)];
15
16// Removing elements
17const withoutFirst = numbers.slice(1);
18const withoutLast = numbers.slice(0, -1);
19const withoutMiddle = numbers.filter((_, index) => index !== 1);
20
21// Modifying elements
22const doubled = numbers.map(x => x * 2);
23const incremented = numbers.map(x => x + 1);
24
25// Sorting
26const sorted = [...numbers].sort();
27const reverseSorted = [...numbers].sort((a, b) => b - a);
28
29console.log('Original array:', numbers); // [1, 2, 3]
30console.log('Doubled:', doubled);        // [2, 4, 6]
31console.log('Sorted:', sorted);          // [1, 2, 3]
32
33// Advanced immutable operations
34function updateArrayElement<T>(
35  array: T[],
36  index: number,
37  newValue: T
38): T[] {
39  return array.map((item, i) => i === index ? newValue : item);
40}
41
42function moveArrayElement<T>(
43  array: T[],
44  fromIndex: number,
45  toIndex: number
46): T[] {
47  const result = [...array];
48  const [removed] = result.splice(fromIndex, 1);
49  result.splice(toIndex, 0, removed);
50  return result;
51}
52
53const fruits = ['apple', 'banana', 'cherry'];
54const updatedFruits = updateArrayElement(fruits, 1, 'blueberry');
55const movedFruits = moveArrayElement(fruits, 0, 2);
56
57console.log('Original:', fruits);       // ['apple', 'banana', 'cherry']
58console.log('Updated:', updatedFruits); // ['apple', 'blueberry', 'cherry']
59console.log('Moved:', movedFruits);     // ['banana', 'cherry', 'apple']

Working with objects

1// ❌ Mutable operations on objects
2const user = { id: 1, name: 'Jan', age: 30 };
3user.age = 31;           // Mutates
4user.email = 'jan@example.com'; // Mutates
5delete user.name;        // Mutates
6
7// ✅ Immutable operations on objects
8const originalUser = { id: 1, name: 'Jan', age: 30 };
9
10// Updating properties
11const olderUser = { ...originalUser, age: 31 };
12
13// Adding properties
14const userWithEmail = { ...originalUser, email: 'jan@example.com' };
15
16// Removing properties
17const { name, ...userWithoutName } = originalUser;
18
19// Nested updates
20interface Address {
21  street: string;
22  city: string;
23  country: string;
24}
25
26interface UserWithAddress {
27  id: number;
28  name: string;
29  address: Address;
30}
31
32const userWithAddress: UserWithAddress = {
33  id: 1,
34  name: 'Jan',
35  address: {
36    street: 'Main St',
37    city: 'Warsaw',
38    country: 'Poland'
39  }
40};
41
42// ❌ Mutable nested update
43userWithAddress.address.city = 'Krakow'; // Mutates
44
45// ✅ Immutable nested update
46const userInKrakow = {
47  ...userWithAddress,
48  address: {
49    ...userWithAddress.address,
50    city: 'Krakow'
51  }
52};
53
54// Helper functions for immutable operations
55function updateProperty<T, K extends keyof T>(
56  obj: T,
57  key: K,
58  value: T[K]
59): T {
60  return { ...obj, [key]: value };
61}
62
63function updateNestedProperty<T>(
64  obj: T,
65  path: string[],
66  value: any
67): T {
68  if (path.length === 1) {
69    return { ...obj, [path[0]]: value };
70  }
71
72  const [firstKey, ...restPath] = path;
73  return {
74    ...obj,
75    [firstKey]: updateNestedProperty(obj[firstKey as keyof T], restPath, value)
76  };
77}
78
79// Using helper functions
80const updatedUser = updateProperty(originalUser, 'age', 35);
81const userInGdansk = updateNestedProperty(
82  userWithAddress,
83  ['address', 'city'],
84  'Gdansk'
85);
86
87console.log('Original:', originalUser);     // { id: 1, name: 'Jan', age: 30 }
88console.log('Updated:', updatedUser); // { id: 1, name: 'Jan', age: 35 }

Immutability libraries

1// Immer - a library that simplifies working with immutable structures
2// npm install immer
3
4import { produce } from 'immer';
5
6interface Todo {
7  id: number;
8  text: string;
9  completed: boolean;
10}
11
12const todos: Todo[] = [
13  { id: 1, text: 'Learn JavaScript', completed: false },
14  { id: 2, text: 'Learn TypeScript', completed: true },
15  { id: 3, text: 'Learn React', completed: false }
16];
17
18// ✅ With Immer - looks like mutation, but creates new state
19const updatedTodos = produce(todos, draft => {
20  const todo = draft.find(t => t.id === 1);
21  if (todo) {
22    todo.completed = true;
23  }
24});
25
26const todosWithNewItem = produce(todos, draft => {
27  draft.push({
28    id: 4,
29    text: 'Learn functional programming',
30    completed: false
31  });
32});
33
34console.log('Original todos:', todos);
35console.log('Updated todos:', updatedTodos);
36console.log('Todos with new item:', todosWithNewItem);
37
38// Advanced example with nested structures
39interface Project {
40  id: number;
41  name: string;
42  todos: Todo[];
43  members: string[];
44}
45
46const project: Project = {
47  id: 1,
48  name: 'Project A',
49  todos: todos,
50  members: ['Jan', 'Anna']
51};
52
53const updatedProject = produce(project, draft => {
54  // Add new member
55  draft.members.push('Piotr');
56
57  // Mark all todos as completed
58  draft.todos.forEach(todo => {
59    todo.completed = true;
60  });
61
62  // Change project name
63  draft.name = 'Project A - Completed';
64});
65
66console.log('Original project:', project);
67console.log('Updated project:', updatedProject);

Currying

Currying is a technique of transforming a function that takes multiple arguments into a series of functions, each taking a single argument.

Basic currying

1// ❌ Function with multiple arguments
2function multiply(a: number, b: number, c: number): number {
3  return a * b * c;
4}
5
6console.log(multiply(2, 3, 4)); // 24
7
8// ✅ Curried version
9function curriedMultiply(a: number) {
10  return function(b: number) {
11    return function(c: number) {
12      return a * b * c;
13    };
14  };
15}
16
17console.log(curriedMultiply(2)(3)(4)); // 24
18
19// ✅ Curried version with arrow functions
20const curriedMultiplyArrow = (a: number) => (b: number) => (c: number) => a * b * c;
21
22console.log(curriedMultiplyArrow(2)(3)(4)); // 24
23
24// Partial application
25const multiplyBy2 = curriedMultiply(2);
26const multiplyBy2And3 = multiplyBy2(3);
27
28console.log(multiplyBy2And3(4)); // 24
29console.log(multiplyBy2And3(5)); // 30

Practical applications of currying

1// Logging system with currying
2type LogLevel = 'info' | 'warn' | 'error';
3
4const logger = (level: LogLevel) => (message: string) => (context?: object) => {
5  const timestamp = new Date().toISOString();
6  const logEntry = {
7    level,
8    message,
9    timestamp,
10    context
11  };
12
13  console.log(JSON.stringify(logEntry, null, 2));
14};
15
16// Creating specialized loggers
17const infoLogger = logger('info');
18const errorLogger = logger('error');
19const warnLogger = logger('warn');
20
21// Usage
22infoLogger('Application started')();
23errorLogger('Database connection error')({ database: 'PostgreSQL' });
24warnLogger('High RAM usage')({ usage: '85%' });
25
26// Validation system with currying
27type Validator<T> = (value: T) => boolean;
28
29const createValidator = <T>(predicate: (value: T) => boolean) =>
30  (errorMessage: string) =>
31  (value: T): { isValid: boolean; error?: string } => {
32    const isValid = predicate(value);
33    return isValid ? { isValid } : { isValid, error: errorMessage };
34  };
35
36// Validation predicates
37const isNotEmpty = (value: string) => value.trim().length > 0;
38const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
39const isMinLength = (minLength: number) => (value: string) => value.length >= minLength;
40const isNumber = (value: string) => !isNaN(Number(value));
41
42// Creating validators
43const requiredValidator = createValidator(isNotEmpty)('Field is required');
44const emailValidator = createValidator(isEmail)('Invalid email format');
45const passwordValidator = createValidator(isMinLength(8))('Password must be at least 8 characters');
46const ageValidator = createValidator(isNumber)('Age must be a number');
47
48// Using validators
49console.log(requiredValidator('Jan'));        // { isValid: true }
50console.log(requiredValidator(''));           // { isValid: false, error: 'Field is required' }
51console.log(emailValidator('jan@example.com')); // { isValid: true }
52console.log(passwordValidator('123'));         // { isValid: false, error: 'Password must be at least 8 characters' }
53
54// API configuration system with currying
55const apiCall = (baseUrl: string) =>
56  (endpoint: string) =>
57  (method: 'GET' | 'POST' | 'PUT' | 'DELETE') =>
58  (data?: any) => {
59    const config = {
60      url: \`\${baseUrl}\${endpoint}\`,
61      method,
62      data
63    };
64
65    console.log('API Call:', config);
66    // Here would be the actual API call
67    return Promise.resolve({ success: true, config });
68  };
69
70// Configuration for different environments
71const devApi = apiCall('https://dev-api.example.com');
72const prodApi = apiCall('https://api.example.com');
73
74// Specialized functions for different endpoints
75const usersApi = devApi('/users');
76const productsApi = devApi('/products');
77
78// Specific operations
79const getUsers = usersApi('GET');
80const createUser = usersApi('POST');
81const updateUser = usersApi('PUT');
82
83// Usage
84getUsers();
85createUser({ name: 'Jan', email: 'jan@example.com' });
86updateUser({ id: 1, name: 'Jan Kowalski' });

Automatic currying

1// Helper function for automatic currying
2function curry<TArgs extends any[], TReturn>(
3  fn: (...args: TArgs) => TReturn
4): (...args: any[]) => any {
5  return function curried(...args: any[]) {
6    if (args.length >= fn.length) {
7      return fn(...args as TArgs);
8    } else {
9      return function(...nextArgs: any[]) {
10        return curried(...args, ...nextArgs);
11      };
12    }
13  };
14}
15
16// Example of automatic currying usage
17function sum(a: number, b: number, c: number, d: number): number {
18  return a + b + c + d;
19}
20
21const curriedSum = curry(sum);
22
23// Various ways to call
24console.log(curriedSum(1, 2, 3, 4));     // 10
25console.log(curriedSum(1)(2)(3)(4));     // 10
26console.log(curriedSum(1, 2)(3, 4));     // 10
27console.log(curriedSum(1)(2, 3)(4));     // 10
28
29// Partial application
30const add5 = curriedSum(5);
31const add5And10 = add5(10);
32const add5And10And15 = add5And10(15);
33
34console.log(add5And10And15(20)); // 50
35
36// Example with filtering and sorting
37function filterAndSort<T>(
38  predicate: (item: T) => boolean,
39  compareKey: keyof T,
40  array: T[]
41): T[] {
42  return array
43    .filter(predicate)
44    .sort((a, b) => {
45      if (a[compareKey] < b[compareKey]) return -1;
46      if (a[compareKey] > b[compareKey]) return 1;
47      return 0;
48    });
49}
50
51const curriedFilterAndSort = curry(filterAndSort);
52
53interface Product {
54  id: number;
55  name: string;
56  price: number;
57  category: string;
58  inStock: boolean;
59}
60
61const products: Product[] = [
62  { id: 1, name: 'Laptop', price: 2000, category: 'Electronics', inStock: true },
63  { id: 2, name: 'Mouse', price: 50, category: 'Electronics', inStock: false },
64  { id: 3, name: 'Book', price: 25, category: 'Books', inStock: true },
65  { id: 4, name: 'Keyboard', price: 150, category: 'Electronics', inStock: true }
66];
67
68// Specialized functions
69const filterInStockSortByPrice = curriedFilterAndSort(
70  (product: Product) => product.inStock
71)('price');
72
73const filterElectronicsSortByName = curriedFilterAndSort(
74  (product: Product) => product.category === 'Electronics'
75)('name');
76
77console.log('Available products sorted by price:', filterInStockSortByPrice(products));
78console.log('Electronics sorted by name:', filterElectronicsSortByName(products));

Function Composition

Function composition is a technique of combining simple functions into more complex ones by passing the result of one function as an argument to the next.

Basic composition

1// Simple functions
2const add = (x: number) => x + 1;
3const multiply = (x: number) => x * 2;
4const square = (x: number) => x * x;
5
6// ❌ Without composition - nested calls
7const result1 = square(multiply(add(5))); // ((5 + 1) * 2)^2 = 144
8
9// ✅ compose function - executes functions from right to left
10function compose<T>(...functions: Array<(arg: T) => T>) {
11  return (value: T): T => {
12    return functions.reduceRight((acc, fn) => fn(acc), value);
13  };
14}
15
16// ✅ pipe function - executes functions from left to right (more intuitive)
17function pipe<T>(...functions: Array<(arg: T) => T>) {
18  return (value: T): T => {
19    return functions.reduce((acc, fn) => fn(acc), value);
20  };
21}
22
23// Using compose
24const composedFunction = compose(square, multiply, add);
25console.log(composedFunction(5)); // 144
26
27// Using pipe (more readable)
28const pipedFunction = pipe(add, multiply, square);
29console.log(pipedFunction(5)); // 144
30
31// Practical example - user data processing
32interface User {
33  firstName: string;
34  lastName: string;
35  age: number;
36  email: string;
37}
38
39const users: User[] = [
40  { firstName: 'Jan', lastName: 'Kowalski', age: 25, email: 'jan@example.com' },
41  { firstName: 'Anna', lastName: 'Nowak', age: 30, email: 'anna@example.com' },
42  { firstName: 'Piotr', lastName: 'Wisniewski', age: 22, email: 'piotr@example.com' }
43];
44
45// Helper functions
46const filterAdults = (users: User[]) => users.filter(user => user.age >= 18);
47const sortByAge = (users: User[]) => [...users].sort((a, b) => a.age - b.age);
48const mapToFullNames = (users: User[]) =>
49  users.map(user => \`\${user.firstName} \${user.lastName}\`);
50
51// Composition
52const processUsers = pipe(
53  filterAdults,
54  sortByAge,
55  mapToFullNames
56);
57
58const processedUsers = processUsers(users);
59console.log(processedUsers); // ['Piotr Wisniewski', 'Jan Kowalski', 'Anna Nowak']

Advanced composition with TypeScript

1// Typed composition for different types
2function compose2<A, B, C>(f: (b: B) => C, g: (a: A) => B) {
3  return (a: A): C => f(g(a));
4}
5
6function compose3<A, B, C, D>(
7  f: (c: C) => D,
8  g: (b: B) => C,
9  h: (a: A) => B
10) {
11  return (a: A): D => f(g(h(a)));
12}
13
14// Example of typed composition usage
15const parseNumber = (str: string): number => parseFloat(str);
16const addTax = (price: number): number => price * 1.23;
17const formatCurrency = (amount: number): string => \`\${amount.toFixed(2)} PLN\`;
18
19const processPrice = compose3(formatCurrency, addTax, parseNumber);
20
21console.log(processPrice('100')); // '123.00 PLN'
22
23// Validation system with composition
24type ValidationResult = {
25  isValid: boolean;
26  errors: string[];
27};
28
29type Validator<T> = (value: T) => ValidationResult;
30
31const combineValidators = <T>(...validators: Validator<T>[]): Validator<T> => {
32  return (value: T): ValidationResult => {
33    const results = validators.map(validator => validator(value));
34
35    return {
36      isValid: results.every(result => result.isValid),
37      errors: results.flatMap(result => result.errors)
38    };
39  };
40};
41
42// Individual validators
43const isRequired: Validator<string> = (value: string) => ({
44  isValid: value.trim().length > 0,
45  errors: value.trim().length === 0 ? ['Field is required'] : []
46});
47
48const isEmail: Validator<string> = (value: string) => {
49  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
50  return {
51    isValid,
52    errors: isValid ? [] : ['Invalid email format']
53  };
54};
55
56const minLength = (min: number): Validator<string> => (value: string) => {
57  const isValid = value.length >= min;
58  return {
59    isValid,
60    errors: isValid ? [] : [\`Minimum \${min} characters required\`]
61  };
62};
63
64// Composition of validators
65const emailValidator = combineValidators(isRequired, isEmail);
66const passwordValidator = combineValidators(isRequired, minLength(8));
67
68console.log(emailValidator('jan@example.com')); // { isValid: true, errors: [] }
69console.log(emailValidator(''));                // { isValid: false, errors: ['Field is required'] }
70console.log(passwordValidator('123'));          // { isValid: false, errors: ['Minimum 8 characters required'] }
71
72// Data processing with composition
73interface RawProduct {
74  name: string;
75  price: string;
76  description: string;
77  tags: string;
78}
79
80interface ProcessedProduct {
81  name: string;
82  price: number;
83  description: string;
84  tags: string[];
85  slug: string;
86}
87
88// Transformation functions
89const parsePrice = (product: RawProduct): ProcessedProduct => ({
90  ...product,
91  price: parseFloat(product.price),
92  tags: [],
93  slug: ''
94});
95
96const parseTags = (product: ProcessedProduct): ProcessedProduct => ({
97  ...product,
98  tags: product.tags ? product.tags.split(',').map(tag => tag.trim()) : []
99});
100
101const generateSlug = (product: ProcessedProduct): ProcessedProduct => ({
102  ...product,
103  slug: product.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
104});
105
106const validateProduct = (product: ProcessedProduct): ProcessedProduct => {
107  if (product.price <= 0) {
108    throw new Error('Price must be greater than 0');
109  }
110  if (!product.name.trim()) {
111    throw new Error('Product name is required');
112  }
113  return product;
114};
115
116// Processing composition
117const processProduct = pipe(
118  parsePrice,
119  parseTags,
120  generateSlug,
121  validateProduct
122);
123
124const rawProduct: RawProduct = {
125  name: 'Super Laptop',
126  price: '2999.99',
127  description: 'The best laptop on the market',
128  tags: 'laptop, computer, electronics'
129};
130
131try {
132  const processed = processProduct(rawProduct);
133  console.log('Processed product:', processed);
134} catch (error) {
135  console.error('Processing error:', error.message);
136}

Asynchronous composition

1// Composition for asynchronous functions
2const composeAsync = <T>(...functions: Array<(arg: T) => Promise<T>>) => {
3  return (value: T): Promise<T> => {
4    return functions.reduceRight(
5      (acc, fn) => acc.then(fn),
6      Promise.resolve(value)
7    );
8  };
9};
10
11const pipeAsync = <T>(...functions: Array<(arg: T) => Promise<T>>) => {
12  return (value: T): Promise<T> => {
13    return functions.reduce(
14      (acc, fn) => acc.then(fn),
15      Promise.resolve(value)
16    );
17  };
18};
19
20// Example usage with API
21interface ApiUser {
22  id: number;
23  name: string;
24  email: string;
25}
26
27interface EnrichedUser extends ApiUser {
28  avatar: string;
29  lastLogin: Date;
30  preferences: any;
31}
32
33// Asynchronous functions
34const fetchUser = async (userId: number): Promise<ApiUser> => {
35  console.log(\`Fetching user \${userId}\`);
36  // API call simulation
37  return new Promise(resolve => {
38    setTimeout(() => {
39      resolve({
40        id: userId,
41        name: 'Jan Kowalski',
42        email: 'jan@example.com'
43      });
44    }, 100);
45  });
46};
47
48const enrichWithAvatar = async (user: ApiUser): Promise<EnrichedUser> => {
49  console.log('Enriching with avatar');
50  return new Promise(resolve => {
51    setTimeout(() => {
52      resolve({
53        ...user,
54        avatar: \`https://avatars.example.com/\${user.id}\`,
55        lastLogin: new Date(),
56        preferences: {}
57      });
58    }, 50);
59  });
60};
61
62const enrichWithPreferences = async (user: EnrichedUser): Promise<EnrichedUser> => {
63  console.log('Enriching with preferences');
64  return new Promise(resolve => {
65    setTimeout(() => {
66      resolve({
67        ...user,
68        preferences: {
69          theme: 'dark',
70          language: 'pl',
71          notifications: true
72        }
73      });
74    }, 50);
75  });
76};
77
78// Asynchronous composition
79const enrichUser = pipeAsync(
80  enrichWithAvatar,
81  enrichWithPreferences
82);
83
84// Usage
85async function main() {
86  try {
87    const user = await fetchUser(1);
88    const enrichedUser = await enrichUser(user);
89    console.log('Enriched user:', enrichedUser);
90  } catch (error) {
91    console.error('Error:', error);
92  }
93}
94
95main();

Practical example - E-commerce system with functional programming

1// Comprehensive example of functional programming application
2
3// Data types
4interface Product {
5  id: string;
6  name: string;
7  price: number;
8  category: string;
9  tags: string[];
10  inStock: boolean;
11  rating: number;
12}
13
14interface CartItem {
15  product: Product;
16  quantity: number;
17}
18
19interface Cart {
20  items: CartItem[];
21  discountCode?: string;
22}
23
24interface Order {
25  id: string;
26  items: CartItem[];
27  subtotal: number;
28  discount: number;
29  tax: number;
30  total: number;
31  timestamp: Date;
32}
33
34// Pure functions for products
35const filterByCategory = (category: string) => (products: Product[]) =>
36  products.filter(product => product.category === category);
37
38const filterByPriceRange = (min: number, max: number) => (products: Product[]) =>
39  products.filter(product => product.price >= min && product.price <= max);
40
41const filterInStock = (products: Product[]) =>
42  products.filter(product => product.inStock);
43
44const sortByPrice = (ascending: boolean = true) => (products: Product[]) =>
45  [...products].sort((a, b) => ascending ? a.price - b.price : b.price - a.price);
46
47const sortByRating = (products: Product[]) =>
48  [...products].sort((a, b) => b.rating - a.rating);
49
50// Cart functions
51const addToCart = (product: Product, quantity: number) => (cart: Cart): Cart => {
52  const existingItemIndex = cart.items.findIndex(item => item.product.id === product.id);
53
54  if (existingItemIndex >= 0) {
55    return {
56      ...cart,
57      items: cart.items.map((item, index) =>
58        index === existingItemIndex
59          ? { ...item, quantity: item.quantity + quantity }
60          : item
61      )
62    };
63  }
64
65  return {
66    ...cart,
67    items: [...cart.items, { product, quantity }]
68  };
69};
70
71const removeFromCart = (productId: string) => (cart: Cart): Cart => ({
72  ...cart,
73  items: cart.items.filter(item => item.product.id !== productId)
74});
75
76const updateQuantity = (productId: string, newQuantity: number) => (cart: Cart): Cart => {
77  if (newQuantity <= 0) {
78    return removeFromCart(productId)(cart);
79  }
80
81  return {
82    ...cart,
83    items: cart.items.map(item =>
84      item.product.id === productId
85        ? { ...item, quantity: newQuantity }
86        : item
87    )
88  };
89};
90
91const applyDiscountCode = (discountCode: string) => (cart: Cart): Cart => ({
92  ...cart,
93  discountCode
94});
95
96// Price calculation functions
97const calculateItemTotal = (item: CartItem): number =>
98  item.product.price * item.quantity;
99
100const calculateSubtotal = (items: CartItem[]): number =>
101  items.reduce((total, item) => total + calculateItemTotal(item), 0);
102
103const calculateDiscount = (subtotal: number, discountCode?: string): number => {
104  const discountRates: Record<string, number> = {
105    'SAVE10': 0.1,
106    'SAVE20': 0.2,
107    'STUDENT': 0.15
108  };
109
110  if (!discountCode || !discountRates[discountCode]) {
111    return 0;
112  }
113
114  return subtotal * discountRates[discountCode];
115};
116
117const calculateTax = (amount: number, taxRate: number = 0.23): number =>
118  amount * taxRate;
119
120const calculateTotal = (subtotal: number, discount: number, tax: number): number =>
121  subtotal - discount + tax;
122
123// Composition for order processing
124const processOrder = (cart: Cart): Order => {
125  const subtotal = calculateSubtotal(cart.items);
126  const discount = calculateDiscount(subtotal, cart.discountCode);
127  const taxableAmount = subtotal - discount;
128  const tax = calculateTax(taxableAmount);
129  const total = calculateTotal(subtotal, discount, tax);
130
131  return {
132    id: 'order_' + Math.random().toString(36).substr(2, 9),
133    items: cart.items,
134    subtotal,
135    discount,
136    tax,
137    total,
138    timestamp: new Date()
139  };
140};
141
142// Search functions with composition
143const searchProducts = (
144  query: string,
145  category?: string,
146  priceRange?: { min: number; max: number },
147  sortBy: 'price' | 'rating' = 'rating'
148) => (products: Product[]): Product[] => {
149  const searchByName = (products: Product[]) =>
150    products.filter(product =>
151      product.name.toLowerCase().includes(query.toLowerCase()) ||
152      product.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
153    );
154
155  const filters = [
156    searchByName,
157    filterInStock
158  ];
159
160  if (category) {
161    filters.push(filterByCategory(category));
162  }
163
164  if (priceRange) {
165    filters.push(filterByPriceRange(priceRange.min, priceRange.max));
166  }
167
168  const sorter = sortBy === 'price' ? sortByPrice() : sortByRating;
169
170  return pipe(...filters, sorter)(products);
171};
172
173// Example usage
174const products: Product[] = [
175  {
176    id: '1',
177    name: 'Laptop Gaming',
178    price: 3500,
179    category: 'electronics',
180    tags: ['gaming', 'laptop', 'high-performance'],
181    inStock: true,
182    rating: 4.8
183  },
184  {
185    id: '2',
186    name: 'Gaming Mouse',
187    price: 150,
188    category: 'electronics',
189    tags: ['gaming', 'mouse', 'rgb'],
190    inStock: true,
191    rating: 4.5
192  },
193  {
194    id: '3',
195    name: 'JavaScript Book',
196    price: 89,
197    category: 'books',
198    tags: ['programming', 'javascript', 'learning'],
199    inStock: false,
200    rating: 4.9
201  }
202];
203
204// Creating cart functionally
205const emptyCart: Cart = { items: [] };
206
207const myCart = pipe(
208  addToCart(products[0], 1),
209  addToCart(products[1], 2),
210  applyDiscountCode('SAVE10')
211)(emptyCart);
212
213console.log('Cart:', myCart);
214
215// Processing the order
216const order = processOrder(myCart);
217console.log('Order:', order);
218
219// Searching products
220const searchGaming = searchProducts('gaming', 'electronics', undefined, 'price');
221const gamingProducts = searchGaming(products);
222console.log('Gaming products:', gamingProducts);
223
224// Curried functions for frequent use
225const addLaptopToCart = addToCart(products[0]);
226const addMouseToCart = addToCart(products[1]);
227
228const cartWithLaptop = addLaptopToCart(1)(emptyCart);
229const cartWithBoth = addMouseToCart(2)(cartWithLaptop);
230
231console.log('Cart with laptop and mouse:', cartWithBoth);

Summary of functional programming

Functional programming in JavaScript/TypeScript offers:

  1. Pure functions - predictability and ease of testing
  2. Immutability - safety and absence of side effects
  3. Currying - flexibility and reuse
  4. Composition - building complex logic from simple elements

Benefits:

  • Code is more predictable and easier to understand
  • Easier unit testing
  • Better capability for parallel processing
  • Reduced number of bugs related to state mutation

When to use:

  • Data processing and transformations
  • Systems with heavy computations
  • Applications requiring high reliability
  • Code requiring frequent testing

Just as mathematicians create elegant proofs by combining simple axioms, functional programming allows building complex systems from simple, reliable elements.

Go to CodeWorlds