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.
A pure function is a function that satisfies two conditions:
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}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);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! 55Immutability means that objects and data structures are not modified after creation. Instead, new versions are created with the applied changes.
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 value1// ❌ 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']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 }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 is a technique of transforming a function that takes multiple arguments into a series of functions, each taking a single argument.
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)); // 301// 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' });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 is a technique of combining simple functions into more complex ones by passing the result of one function as an argument to the next.
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']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}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();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);Functional programming in JavaScript/TypeScript offers:
Benefits:
When to use:
Just as mathematicians create elegant proofs by combining simple axioms, functional programming allows building complex systems from simple, reliable elements.