| name | fp-ts |
| description | Master the fp-ts library for typed functional programming in TypeScript, including Option, Either, Task, TaskEither, Reader, State, IO, Array, Record, pipe/flow composition, Do notation, optics (lenses/prisms), and integration with the Effect-TS ecosystem. Use when working with fp-ts data types, composing functional pipelines, handling effects functionally, implementing monadic patterns, or using fp-ts utilities for type-safe functional code. |
fp-ts Mastery
fp-ts is the most widely used library for typed functional programming in TypeScript, bringing abstractions from Haskell and Scala with strict type safety.
Installation and Setup
npm install fp-ts
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import * as TE from 'fp-ts/TaskEither';
import { pipe, flow } from 'fp-ts/function';
Core Concepts
pipe and flow
The foundation of fp-ts composition.
import { pipe, flow } from 'fp-ts/function';
const result = pipe(
5,
n => n * 2,
n => n + 1,
n => n.toString()
);
const processNumber = flow(
(n: number) => n * 2,
n => n + 1,
n => n.toString()
);
processNumber(5);
The HKT (Higher-Kinded Types) System
fp-ts uses a sophisticated type system for generic abstractions.
import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT';
const optionURI = 'Option';
type OptionURI = typeof optionURI;
type OptionKind<A> = Kind<OptionURI, A>;
Option Type
Represents optional values without null/undefined.
Construction
import * as O from 'fp-ts/Option';
const some = O.some(42);
const none = O.none;
const fromNullable = O.fromNullable(maybeValue);
const fromPredicate = O.fromPredicate((n: number) => n > 0)(5);
Core Operations
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
pipe(
O.some(5),
O.map(n => n * 2)
);
pipe(
O.some(5),
O.flatMap(n => n > 0 ? O.some(n * 2) : O.none)
);
pipe(
O.none,
O.getOrElse(() => 0)
);
pipe(
O.some(5),
O.fold(
() => 'No value',
n => `Value: ${n}`
)
);
pipe(
O.some(5),
O.filter( n > )
);
(
O.,
O.( O.())
);
Advanced Patterns
import * as A from 'fp-ts/Array';
const options = [O.some(1), O.some(2), O.some(3)];
pipe(
options,
A.sequence(O.Applicative)
);
pipe(
[O.some(1), O.none, O.some(3)],
A.sequence(O.Applicative)
);
pipe(
O.Do,
O.bind('x', () => O.some(5)),
O.bind('y', () => O.some(3)),
O.map(({ x, y }) => x + y)
);
pipe(
O.some(5),
O.exists(n => n > 3)
);
(
O.(),
O.
);
(
O.,
O.
);
Common Use Cases
const head = <A>(arr: readonly A[]): O.Option<A> =>
pipe(arr, A.head);
const getProp = <K extends string>(key: K) =>
<T extends Record<K, unknown>>(obj: T): O.Option<T[K]> =>
O.fromNullable(obj[key]);
const parseNumber = (s: string): O.Option<number> =>
pipe(
O.tryCatch(() => {
const n = parseFloat(s);
return isNaN(n) ? null : n;
})
);
type User = { name: string; address?: { city?: string } };
const getCity = (user: User): O.Option<string> =>
pipe(
user.address,
O.fromNullable,
O.flatMap( => O.(addr.))
);
Either Type
Represents computations that can fail.
Construction
import * as E from 'fp-ts/Either';
const right = E.right(42);
const left = E.left('error');
const fromPredicate = E.fromPredicate(
(n: number) => n > 0,
n => `${n} is not positive`
)(5);
Core Operations
import { pipe } from 'fp-ts/function';
import * as E from 'fp-ts/Either';
pipe(
E.right(5),
E.map(n => n * 2)
);
pipe(
E.left('error'),
E.mapLeft(e => e.toUpperCase())
);
pipe(
E.right(5),
E.flatMap(n => n > 0 ? E.right(n * 2) : E.left('negative'))
);
pipe(
E.right(5),
E.fold(
error => `Error: ${error}`,
value => `Success: ${value}`
)
);
(
E.(),
E.( )
);
(
E.(),
E.( E.())
);
(
E.(),
E.
);
(
E.<, >(),
E.(
e.(),
n *
)
);
Error Handling Patterns
const safeParse = (json: string): E.Either<Error, unknown> =>
E.tryCatch(
() => JSON.parse(json),
reason => new Error(`Parse error: ${reason}`)
);
const validateEmail = (email: string): E.Either<string, string> =>
email.includes('@')
? E.right(email)
: E.left('Invalid email');
const validateAge = (age: number): E.Either<string, number> =>
age >= 18
? E.right(age)
: E.left('Must be 18 or older');
const validateUser = (email: string, age: number): E.Either<string, User> =>
pipe(
validateEmail(email),
E.flatMap(
(
(age),
E.( ({ : validEmail, : validAge }))
)
)
);
validateUserDo = (: , : ): E.<, > =>
(
E.,
E.(, (email)),
E.(, (age))
);
Combining Multiple Eithers
import * as A from 'fp-ts/Array';
import { sequenceT } from 'fp-ts/Apply';
const eithers: E.Either<string, number>[] = [
E.right(1),
E.right(2),
E.right(3)
];
pipe(
eithers,
A.sequence(E.Applicative)
);
pipe(
sequenceT(E.Applicative)(
validateEmail('test@example.com'),
validateAge(25)
)
);
pipe(
E.right(5),
E.toOption
);
pipe(
E.left('error'),
E.toOption
);
Task and TaskEither
Handle asynchronous operations.
Task
Lazy Promise (only executes when called).
import * as T from 'fp-ts/Task';
import { pipe } from 'fp-ts/function';
const delay = (ms: number): T.Task<void> =>
() => new Promise(resolve => setTimeout(resolve, ms));
const fetchData = (): T.Task<Data> =>
() => fetch('/api/data').then(r => r.json());
pipe(
fetchData(),
T.map(data => data.items.length)
);
pipe(
fetchData(),
T.flatMap(data =>
pipe(
delay(1000),
T.map(() => data)
)
)
);
const task = fetchData();
().( .(data));
TaskEither
Asynchronous operations that can fail.
import * as TE from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';
const fetchUser = (id: number): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => {
if (!r.ok) throw new Error('Not found');
return r.json();
}),
reason => new Error(`Fetch failed: ${reason}`)
);
pipe(
fetchUser(1),
TE.map(user => user.name)
);
pipe(
fetchUser(1),
.( ({ : error., : }))
);
(
(),
.( (user.))
);
(
(),
.(
T.(),
T.()
)
)();
(
(),
.( T.(defaultUser))
)();
(
(),
.( ())
);
Do Notation with TaskEither
const processUser = (id: number): TE.TaskEither<Error, Result> =>
pipe(
TE.Do,
TE.bind('user', () => fetchUser(id)),
TE.bind('posts', ({ user }) => fetchPosts(user.id)),
TE.bind('comments', ({ posts }) => fetchComments(posts[0].id)),
TE.map(({ user, posts, comments }) => ({
user,
postCount: posts.length,
commentCount: comments.length
}))
);
Parallel Execution
import { sequenceT } from 'fp-ts/Apply';
import { sequenceArray } from 'fp-ts/Array';
const fetchUserData = (id: number): TE.TaskEither<Error, UserData> =>
pipe(
sequenceT(TE.ApplicativePar)(
fetchUser(id),
fetchPosts(id),
fetchComments(id)
),
TE.map(([user, posts, comments]) => ({
user,
posts,
comments
}))
);
const fetchUsers = (ids: number[]): TE.TaskEither<Error, User[]> =>
pipe(
ids.map(fetchUser),
TE.sequenceArray
);
const fetchUsersSeq = (ids: number[]): TE.TaskEither<Error, User[]> =>
pipe(
ids.map(fetchUser),
A.(.)
);
Array Operations
fp-ts provides powerful array utilities.
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
pipe([1, 2, 3], A.head);
pipe([], A.head);
pipe([1, 2, 3], A.tail);
pipe(
[1, 2, 3, 4, 5],
A.filter(n => n % 2 === 0)
);
pipe(
[1, 2, 3, 4, 5],
A.partition(n => n % 2 === 0)
);
pipe(
['1', , , , ],
A.( {
n = (s);
(n) ? O. : O.(n);
})
);
(
[, , ],
A.( [n, n * ])
);
(
[, , , , ],
A.(, acc + n)
);
(
[, , , , ],
A.( n > )
);
(
[, , ],
A.()
);
(
[, , , , , , ],
A.(.)
);
* ;
(
[, , , , ],
A.(.)
);
(
[, , , ],
A.( s[])
);
(
A.([, , ], [, , ])
);
(
[, , , , , , ],
A.()
);
Traversing with Effects
const parseNumbers = (strs: string[]): O.Option<number[]> =>
pipe(
strs,
A.traverse(O.Applicative)(s => {
const n = parseInt(s);
return isNaN(n) ? O.none : O.some(n);
})
);
parseNumbers(['1', '2', '3']);
parseNumbers(['1', 'foo', '3']);
const validateAll = (
users: UnvalidatedUser[]
): E.Either<string, User[]> =>
pipe(
users,
A.traverse(E.Applicative)(validateUser)
);
const fetchAllUsers = (ids: number[]): TE.TaskEither<Error, User[]> =>
pipe(
ids,
A.traverse(TE.ApplicativePar)(fetchUser)
);
Record Operations
Work with objects functionally.
import * as R from 'fp-ts/Record';
import { pipe } from 'fp-ts/function';
pipe(
{ a: 1, b: 2, c: 3 },
R.map(n => n * 2)
);
pipe(
{ a: 1, b: 2, c: 3 },
R.filter(n => n > 1)
);
pipe(
{ a: '1', b: 'foo', c: '2' },
R.filterMap(s => {
const n = parseInt(s);
return isNaN(n) ? O.none : O.some(n);
})
);
pipe(
{ a: 1, : },
R.()
);
(
{ : , : },
R.()
);
R.({ : , : , : });
R.( [k, v])({ : , : });
* A ;
(
[[, ], [, ], [, ]],
R.(
{ : y },
A.
)
);
validateRecord = (
: <, >
): E.<, <, >> =>
(
record,
R.(E.)( {
n = (s);
(n)
? E.()
: E.(n);
})
);
Reader Monad
Thread configuration/dependencies through computations.
import * as R from 'fp-ts/Reader';
import { pipe } from 'fp-ts/function';
type Config = {
apiUrl: string;
timeout: number;
};
const getApiUrl: R.Reader<Config, string> =
config => config.apiUrl;
const getTimeout: R.Reader<Config, number> =
config => config.timeout;
const getFullUrl = (path: string): R.Reader<Config, string> =>
pipe(
getApiUrl,
R.map(url => `${url}${path}`)
);
const fetchWithTimeout = (path: string): R.Reader<Config, Promise<Response>> =>
pipe(
R.Do,
R.(, (path)),
R.(, getTimeout),
R.(
(url, { : .(timeout) })
)
);
: R.<, > =
(
R.<>(),
R.( .(config))
);
withDifferentUrl = <A>(
: R.<, A>
): R.<, A> =>
(
reader,
R.( ({
...config,
:
}))
);
: = {
: ,
:
};
result = ()(config);
ReaderTaskEither
Combine Reader, Task, and Either for dependency injection with async error handling.
import * as RTE from 'fp-ts/ReaderTaskEither';
import { pipe } from 'fp-ts/function';
type Deps = {
db: Database;
logger: Logger;
config: Config;
};
const getUser = (id: number): RTE.ReaderTaskEither<Deps, Error, User> =>
pipe(
RTE.ask<Deps>(),
RTE.flatMap(({ db, logger }) =>
RTE.tryCatch(
async () => {
logger.info(`Fetching user ${id}`);
return db.users.findById(id);
},
reason => new Error(`Failed to fetch user: ${reason}`)
)
)
);
const getUserWithPosts = (
id:
): .<, , > =>
(
.,
.(, (id)),
.(, (user.)),
.( ({ ...user, posts }))
);
: = {
: (),
: (),
: ()
};
()(deps)()
.(E.(
.(error),
.(user)
));
withTestDb = <A>(
: .<, , A>
): .<, , A> =>
(
rte,
.( ({
...deps,
: ()
}))
);
State Monad
Thread state through computations.
import * as S from 'fp-ts/State';
import { pipe } from 'fp-ts/function';
type Counter = { count: number };
const increment: S.State<Counter, number> =
state => [state.count + 1, { count: state.count + 1 }];
const decrement: S.State<Counter, number> =
state => [state.count - 1, { count: state.count - 1 }];
const getCount: S.State<Counter, number> =
state => [state.count, state];
const setCount = (count: number): S.State<Counter, void> =>
_state => [undefined, { count }];
multiplyCount = (: ): S.<, > =>
S.( ({ : state. * factor }));
: S.<, > =
(
S.,
S.(, getCount),
S.(, increment),
S.(, increment),
S.(, {
();
getCount;
}),
S.(
)
);
: = { : };
[result, finalState] = (initialState);
IO Monad
Encapsulate side effects.
import * as IO from 'fp-ts/IO';
import { pipe } from 'fp-ts/function';
const log = (message: string): IO.IO<void> =>
() => console.log(message);
const random: IO.IO<number> =
() => Math.random();
const now: IO.IO<Date> =
() => new Date();
pipe(
random,
IO.map(n => n * 100)
);
pipe(
random,
IO.flatMap(n => log(`Random: ${n}`))
);
const : .<> =
(
.,
.(, now),
.(, random),
.( ()),
.( )
);
();
Optics (Lenses, Prisms, Traversals)
Access and modify nested data structures immutably.
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import { Lens, Optional, Prism } from 'monocle-ts';
type Address = {
street: string;
city: string;
zipCode: string;
};
type Person = {
name: string;
age: number;
address: Address;
};
const addressLens = Lens.fromProp<Person>()('address');
const cityLens = Lens.fromProp<Address>()('city');
const personCityLens = pipe(addressLens, Lens.compose(cityLens));
const person: Person = {
name: 'John',
age: 30,
address: { : , : , : }
};
personCityLens.(person);
updated = personCityLens.()(person);
capitalized = personCityLens.( city.())(person);
= {
: ;
?: ;
};
emailOptional = .<>()();
: = { : , : };
emailOptional.(user);
emailOptional.()(user);
=
| { : ; : }
| { : ; : ; : };
circlePrism = .((: ): s is <, { : }> =>
s. ===
);
: = { : , : };
circlePrism.(shape);
circlePrism.( ({ ...c, : c. * }))(shape);
{ } ;
* A ;
arrayTraversal = <A> .(A.)<A>();
numbers = [, , , , ];
(
numbers,
arrayTraversal<>().( n * )
);
Eq, Ord, and Semigroup
Type classes for comparison and combination.
import * as Eq from 'fp-ts/Eq';
import * as Ord from 'fp-ts/Ord';
import * as S from 'fp-ts/Semigroup';
import { pipe } from 'fp-ts/function';
const eqPerson = Eq.struct<Person>({
name: Eq.eqString,
age: Eq.eqNumber,
address: Eq.struct({
street: Eq.eqString,
city: Eq.eqString,
zipCode: Eq.eqString
})
});
eqPerson.equals(person1, person2);
const ordPerson = pipe(
Ord.ordNumber,
Ord.contramap((p: Person) => p.age)
);
people = [person1, person2, person3];
(people, A.(ordPerson));
semigroupSum = S.;
S.(semigroupSum)()([, , , ]);
semigroupProduct = S.;
S.(semigroupProduct)()([, , ]);
= { : ; : };
: S.<> = {
: ({
: ,
: .(x., y.)
})
};
* M ;
monoidString = M.;
M.(monoidString)([, , ]);
Practical Patterns
API Request Pipeline
import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
type ApiError =
| { type: 'NetworkError'; message: string }
| { type: 'ParseError'; message: string }
| { type: 'ValidationError'; errors: string[] };
const request = <A>(
url: string,
options?: RequestInit
): TE.TaskEither<ApiError, A> =>
pipe(
TE.tryCatch(
() => fetch(url, options),
reason => ({
type: 'NetworkError' as const,
message: String(reason)
})
),
TE.flatMap(response =>
TE.tryCatch(
response.(),
({
: ,
: (reason)
})
)
)
);
validateUser = (: ): E.<, > => {
((data)) {
E.(data );
}
E.({
: ,
: []
});
};
fetchUser = (: ): .<, > =>
(
request<>(),
.(validateUser)
);
Form Validation
import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { pipe } from 'fp-ts/function';
import { sequenceT } from 'fp-ts/Apply';
type ValidationError = {
field: string;
message: string;
};
type Validation<A> = E.Either<ValidationError[], A>;
const validateRequired = (field: string) => (value: string): Validation<string> =>
value.length > 0
? E.right(value)
: E.left([{ field, message: 'Required' }]);
const validateEmail = (value: string): Validation<string> =>
value.includes('@')
? E.right(value)
: E.left([{ field: 'email', message: 'Invalid email' }]);
validateAge = (: ): <> =>
value >=
? E.(value)
: E.([{ : , : }]);
* ;
getValidationApplicative = <E>(): .<, E[]> => ({
...E.,
:
(
fab,
E.(
(
fa,
E.(f),
E.(
(
fab,
E.( [...e2, ...e1]),
E.,
O.((): E[] => e1)
)
)
)
)
)
});
validateForm = (
: ,
:
): <{ : ; : }> =>
(
(getValidationApplicative<>())(
(email, (), E.(validateEmail)),
(age)
),
E.( ({ email, age }))
);
Dependency Injection
import * as RTE from 'fp-ts/ReaderTaskEither';
import { pipe } from 'fp-ts/function';
type Services = {
userRepo: UserRepository;
emailService: EmailService;
logger: Logger;
};
class UserService {
getUser(id: number): RTE.ReaderTaskEither<Services, Error, User> {
return pipe(
RTE.ask<Services>(),
RTE.flatMap(({ userRepo, logger }) =>
RTE.tryCatch(
async () => {
logger.info(`Fetching user ${id}`);
return userRepo.findById(id);
},
e => new Error(`Failed: ${e}`)
)
)
);
}
createUser(data: ): .<, , > {
(
.,
.(, .<>()),
.(,
.(
services..(data),
()
)
),
.(
.(
services..(user.)
)
),
.( user)
);
}
}
: = {
: (),
: (),
: ()
};
userService = ();
userService.(userData)(services)()
.(E.(
.(error),
.(, user)
));
Best Practices
- Use pipe for data flow: Always use
pipe for left-to-right data transformation
- Leverage Do notation: Use Do notation for imperative-style sequencing when clearer
- Choose appropriate effects: Use TaskEither for async+errors, Reader for DI, IO for side effects
- Prefer traverse over manual loops: Use
traverse and sequence for effectful operations
- Type your errors explicitly: Use discriminated unions for error types
- Use Applicative for parallel: Use
ApplicativePar for parallel execution
- Compose with flow: Use
flow to create reusable function compositions
- Avoid nesting: Flatten nested structures with
flatMap
- Use type classes: Leverage Eq, Ord, Semigroup, Monoid for generic operations
- Test with property-based testing: fp-ts types work great with fast-check
Common Patterns
Error Recovery
const fetchWithRetry = <A>(
fetch: TE.TaskEither<Error, A>,
maxRetries: number
): TE.TaskEither<Error, A> => {
const retry = (n: number): TE.TaskEither<Error, A> =>
pipe(
fetch,
TE.orElse(error =>
n > 0
? pipe(
T.delay(1000)(T.of(undefined)),
TE.fromTask,
TE.flatMap(() => retry(n - 1))
)
: TE.left(error)
)
);
return retry(maxRetries);
};
Caching
const cached = <A>(
fetch: TE.TaskEither<Error, A>
): TE.TaskEither<Error, A> => {
let cache: O.Option<A> = O.none;
return pipe(
cache,
O.fold(
() =>
pipe(
fetch,
TE.map(value => {
cache = O.some(value);
return value;
})
),
value => TE.right(value)
)
);
};
Resource Management
const bracket = <R, E, A>(
acquire: TE.TaskEither<E, R>,
use: (r: R) => TE.TaskEither<E, A>,
release: (r: R) => TE.TaskEither<E, void>
): TE.TaskEither<E, A> =>
pipe(
acquire,
TE.flatMap(resource =>
pipe(
use(resource),
TE.chainFirst(() => release(resource))
)
)
);
Integration with Effect-TS
fp-ts is evolving as part of the Effect-TS ecosystem, which provides even richer effect and functional abstractions. Consider Effect for new projects requiring advanced features like fiber-based concurrency, structured concurrency, resource management, and more sophisticated effect systems.
import { Effect } from 'effect';
const program = Effect.gen(function* (_) {
const user = yield* _(fetchUser(1));
const posts = yield* _(fetchPosts(user.id));
return { user, posts };
});