Lab 09: FP with TypeScript

Time: 40 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Apply functional programming patterns in TypeScript using fp-ts: Option for nullable values, Either for error handling, pipe/flow for composition, and TaskEither for async operations.


Step 1: Environment Setup

docker run -it --rm node:20-alpine sh
npm install -g typescript ts-node
mkdir lab09 && cd lab09
npm init -y
npm install fp-ts
echo '{"compilerOptions":{"module":"commonjs","target":"es2020","strict":true,"esModuleInterop":true}}' > tsconfig.json

💡 fp-ts uses "Higher-Kinded Types" (HKT) simulation in TypeScript. It brings Haskell/Scala-style FP to TypeScript without any runtime overhead — it's pure type manipulation.


Step 2: Option — Safe Nullable Values

Option<A> is either Some<A> (has a value) or None (no value). It's a type-safe alternative to null | undefined:

// option.ts
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';

// Creating Options
const some = O.some(42);           // Option<number> with value
const none = O.none;               // Option<never> — no value
const fromNull = O.fromNullable(null);  // None
const fromValue = O.fromNullable('hello'); // Some('hello')

// Transforming Options
const database: Record<string, { name: string; age: number }> = {
  'u1': { name: 'Alice', age: 30 },
  'u2': { name: 'Bob', age: 25 },
};

function findUser(id: string): O.Option<{ name: string; age: number }> {
  return O.fromNullable(database[id]);
}

function getAge(user: { name: string; age: number }): number {
  return user.age;
}

// pipe chains operations left-to-right
const result1 = pipe(
  findUser('u1'),
  O.map(getAge),                      // Transform if Some
  O.filter(age => age >= 18),          // Keep if predicate passes
  O.map(age => `Adult, age: ${age}`),
  O.getOrElse(() => 'Unknown user'),   // Extract with fallback
);

const result2 = pipe(
  findUser('u999'),  // Not found → None
  O.map(getAge),
  O.getOrElse(() => 0),
);

console.log(result1); // "Adult, age: 30"
console.log(result2); // 0

// chain (flatMap) — when the transformation itself returns Option
function findEmail(name: string): O.Option<string> {
  const emails: Record<string, string> = { Alice: '[email protected]' };
  return O.fromNullable(emails[name]);
}

const email = pipe(
  findUser('u1'),
  O.chain(user => findEmail(user.name)),  // Option<string>
  O.fold(
    () => 'No email found',
    email => `Email: ${email}`,
  ),
);
console.log(email); // "Email: [email protected]"

Step 3: Either — Typed Error Handling

Either<E, A> is Left<E> (error) or Right<A> (success). Unlike try/catch, errors are typed:

💡 E.Do + E.bind is like async/await but for Either. It sequences operations and short-circuits on the first Left.


Step 4: pipe and flow

Compose functions elegantly:


Step 5: Either mapLeft and bimap

Transform both error and success channels:


Step 6: TaskEither for Async Operations

TaskEither<E, A> is a lazy async operation that either fails with E or succeeds with A:


Step 7: Combining Option and Either


Step 8: Capstone — Type-Safe Data Pipeline

Run:

📸 Verified Output:


Summary

Concept
fp-ts API
Use Case

Nullable safety

O.some/none/fromNullable

Replace T | null

Transform Option

O.map/chain/filter/fold

Process optional values

Typed errors

E.left/right

Replace throw

Transform Either

E.map/chain/mapLeft/bimap/fold

Process results

Function composition

pipe(value, f1, f2...)

Sequential transforms

Reusable pipeline

flow(f1, f2, f3)

Create transformer function

Async Either

TE.TaskEither

Promise with typed errors

Wrap promise

TE.tryCatch(promise, errFn)

Safe async operations

Last updated