Skip to content

map()

Transform the result of a task with a mapping function.

Syntax

typescript
task.map<U>(fn: (value: T) => U | Promise<U>): FuturableTask<U>

Parameters

fn

A function that transforms the task's result. Can be synchronous or asynchronous.

  • Input: The resolved value of type T
  • Output: The transformed value of type U (or Promise<U>)

Return Value

A new FuturableTask<U> that resolves with the transformed value.

Description

The map() method creates a new task that applies a transformation function to the result of the original task. This is the core method for functional composition, allowing you to chain multiple transformations.

Key Characteristics

  • Pure Transformation: Should not have side effects (use tap() for that)
  • Type-Safe: Full TypeScript support for type transformations
  • Lazy: Transformation only happens when run() is called
  • Chainable: Returns a new task that can be further transformed
  • Error Propagation: Errors bypass the transformation

Examples

Basic Transformation

typescript
const task = FuturableTask
  .of(() => 5)
  .map(x => x * 2);

const result = await task.run(); // 10

Async Transformation

typescript
const task = FuturableTask
  .of(() => fetch('/api/user'))
  .map(response => response.json())
  .map(user => user.name);

const name = await task.run(); // "John Doe"

Multiple Transformations

typescript
const task = FuturableTask
  .of(() => [1, 2, 3, 4, 5])
  .map(arr => arr.filter(x => x > 2))  // [3, 4, 5]
  .map(arr => arr.map(x => x * 2))     // [6, 8, 10]
  .map(arr => arr.reduce((a, b) => a + b, 0)); // 24

const result = await task.run(); // 24

Object Transformation

typescript
interface User {
  id: number;
  firstName: string;
  lastName: string;
}

const task = FuturableTask
  .of(() => fetch('/api/user'))
  .map(res => res.json())
  .map((user: User) => ({
    id: user.id,
    fullName: `${user.firstName} ${user.lastName}`
  }));

const transformed = await task.run();
// { id: 1, fullName: "John Doe" }

Type Conversion

typescript
// String to number
const task1 = FuturableTask
  .of(() => "42")
  .map(str => parseInt(str, 10));

// Number to formatted string
const task2 = FuturableTask
  .of(() => 1234.56)
  .map(num => num.toFixed(2))
  .map(str => `$${str}`);

await task2.run(); // "$1234.56"

API Response Processing

typescript
const fetchUsers = FuturableTask
  .fetch('/api/users')
  .map(res => res.json())
  .map(users => users.filter(u => u.active))
  .map(users => users.sort((a, b) => a.name.localeCompare(b.name)))
  .map(users => users.slice(0, 10));

const topUsers = await fetchUsers.run();

Data Enrichment

typescript
const enrichUser = FuturableTask
  .of(() => fetchUser(123))
  .map(user => ({
    ...user,
    displayName: `${user.firstName} ${user.lastName}`,
    age: calculateAge(user.birthDate),
    isAdult: calculateAge(user.birthDate) >= 18
  }));

Error Handling

typescript
// map() is skipped if the task fails
const task = FuturableTask
  .of(() => {
    throw new Error('Failed');
  })
  .map(x => x * 2); // This never executes

try {
  await task.run();
} catch (error) {
  console.log(error.message); // "Failed"
}

With Retry

typescript
const resilientTask = FuturableTask
  .fetch('/api/data')
  .map(res => res.json())
  .retry(3)
  .map(data => processData(data));

Conditional Transformation

typescript
const task = FuturableTask
  .of(() => fetchValue())
  .map(value => value > 100 ? 'high' : 'low')
  .map(category => `Category: ${category}`);

Practical Examples

Form Data Processing

typescript
const submitForm = FuturableTask
  .of(() => {
    const formData = new FormData(form);
    return Object.fromEntries(formData);
  })
  .map(data => ({
    ...data,
    email: data.email.toLowerCase(),
    phone: data.phone.replace(/\D/g, '')
  }))
  .map(data => JSON.stringify(data))
  .flatMap(json =>
    FuturableTask.fetch('/api/submit', {
      method: 'POST',
      body: json,
      headers: { 'Content-Type': 'application/json' }
    })
  )
  .map(res => res.json());

Image Processing

typescript
const processImage = FuturableTask
  .of(() => loadImage(url))
  .map(img => resizeImage(img, 800, 600))
  .map(img => applyFilter(img, 'sepia'))
  .map(img => addWatermark(img, logo))
  .map(img => img.toDataURL());

CSV to JSON

typescript
const csvToJson = FuturableTask
  .of(() => readFile('data.csv'))
  .map(content => content.split('\n'))
  .map(lines => lines.filter(line => line.trim()))
  .map(lines => {
    const headers = lines[0].split(',');
    const rows = lines.slice(1);
    return rows.map(row => {
      const values = row.split(',');
      return headers.reduce((obj, header, i) => {
        obj[header] = values[i];
        return obj;
      }, {});
    });
  });

Price Calculation

typescript
const calculateTotal = FuturableTask
  .of(() => fetchCartItems())
  .map(items => items.map(item => item.price * item.quantity))
  .map(prices => prices.reduce((sum, price) => sum + price, 0))
  .map(subtotal => subtotal * 1.1) // Add 10% tax
  .map(total => total.toFixed(2))
  .map(total => `$${total}`);

Comparison with flatMap()

typescript
// map() - for value transformations
const task1 = FuturableTask
  .of(() => 5)
  .map(x => x * 2); // Returns 10

// flatMap() - for task transformations
const task2 = FuturableTask
  .of(() => 5)
  .flatMap(x => FuturableTask.of(() => x * 2)); // Returns 10

// map() with task returns nested task (wrong!)
const task3 = FuturableTask
  .of(() => 5)
  .map(x => FuturableTask.of(() => x * 2));
// Returns FuturableTask<FuturableTask<number>> ❌

Type Transformations

typescript
// String to number
FuturableTask.of(() => "42")
  .map(str => parseInt(str)); // FuturableTask<number>

// Number to boolean
FuturableTask.of(() => 42)
  .map(num => num > 0); // FuturableTask<boolean>

// Object to array
FuturableTask.of(() => ({ a: 1, b: 2 }))
  .map(obj => Object.values(obj)); // FuturableTask<number[]>

// Array to object
FuturableTask.of(() => [['a', 1], ['b', 2]])
  .map(entries => Object.fromEntries(entries)); // FuturableTask<object>

Best Practices

1. Keep Transformations Pure

typescript
// ✅ Good - pure function
.map(x => x * 2)

// ❌ Bad - side effect
.map(x => {
  console.log(x); // Side effect
  return x * 2;
})

// ✅ Use tap() for side effects
.tap(x => console.log(x))
.map(x => x * 2)

2. Chain Multiple Maps

typescript
// ✅ Good - clear chain of transformations
FuturableTask.of(() => data)
  .map(d => validate(d))
  .map(d => normalize(d))
  .map(d => transform(d))

// ❌ Bad - doing everything in one map
.map(d => transform(normalize(validate(d))))

3. Use Type Annotations When Needed

typescript
// ✅ Explicit type for clarity
.map((user: User) => ({
  id: user.id,
  name: user.name
}))

// Or let TypeScript infer
.map(user => ({
  id: user.id,
  name: user.name
}))

4. Handle Null/Undefined

typescript
// ✅ Good - handle null values
.map(value => value ?? defaultValue)
.map(value => value.toUpperCase())

// ❌ Bad - may throw
.map(value => value.toUpperCase()) // Error if null

Error Handling

typescript
// Transformation throws error
const task = FuturableTask
  .of(() => "not a number")
  .map(str => {
    const num = parseInt(str);
    if (isNaN(num)) {
      throw new Error('Invalid number');
    }
    return num;
  });

const safe = task
  .fallbackTo(error => {
    console.error(error);
    return 0;
  });

Performance Considerations

typescript
// ✅ Efficient - transformations only on success
FuturableTask.of(() => expensiveOperation())
  .map(result => transform1(result))
  .map(result => transform2(result))
  .retry(3);

// Transformations only run on successful attempts

// ❌ Inefficient - computing in source
FuturableTask.of(() => {
  const result = expensiveOperation();
  return transform2(transform1(result));
}).retry(3);
// Transformations run on every retry attempt

Notes

  • map() is lazy - transformation only happens when run() is called
  • If the task fails, map() is skipped
  • Can be chained multiple times for complex transformations
  • Use flatMap() when the transformation returns a FuturableTask
  • Use tap() for side effects
  • Transformations are applied in order
  • Each map() creates a new task (original is unchanged)

See Also

Released under the MIT License.