| name | typescript-fp |
| description | Master functional programming in TypeScript with type-safe patterns, strict typing, advanced type system features, discriminated unions, mapped types, conditional types, and functional patterns. Use when writing TypeScript code with functional paradigms, type-safe error handling with Option/Either types, implementing type-safe composition, leveraging TypeScript's type system for functional patterns, or ensuring compile-time safety in functional code. |
Functional Programming in TypeScript
The Zen of Functional Programming in TypeScript
Types are truth
The type system is your first and best test; trust it, extend it, let it guide refactoring.
Functions, not methods
Favor pure, standalone functions over mutating methods. Logic and data are better apart.
Compose, don't construct
Build complex behavior by wiring small functions, not by inheritance or sprawling classes.
Immutability is clarity
Changing data increases entropy; prefer readonly and new values.
Explicit is safety
Encode nullable, optional, and error-prone states as explicit types: Option, Either, custom unions.
Pipelines flow clearly
Use composition (pipe, flow) to make data transformations readable and reasoned about at a glance.
Effects are contained
Push side effects to the edge. Pure in the middle, async/io only when necessary and clearly isolated.
Type errors are friends
If code doesn't type-check, it's not safe—fix it at compile time, not at runtime.
Libraries are bridges
Leverage fp-ts, purify, and effect libraries—they bring the best of ML/Haskell/Scala worlds without leaving TypeScript.
The simplest solution wins
If achievable by composing two pure functions, resist the urge for classes, frameworks, or lifecycles.
There is no 'any' in zen
Avoid any: prefer unknown, never, or expressive unions. Demand and supply information with care.
Pattern match with safety
Discriminated unions and pattern matching are clearer than chains of if/else or error-prone casts.
Refactor fearlessly
Small, pure, and well-typed functions invite confident change—use types as a foundation for safety.
Embrace strictness
strict: true and linting are not obstacles—they are the clearest guides on the path to quality.
TypeScript Configuration for FP
tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
ESLint Configuration
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:functional/recommended'
],
plugins: ['@typescript-eslint', 'functional'],
rules: {
'functional/no-let': 'error',
'functional/no-loop-statement': 'error',
'functional/no-mutation': 'error',
'functional/prefer-readonly-type': 'error',
'functional/immutable-data': 'error',
'functional/no-throw-statement': 'error',
'functional/no-class': 'warn',
'functional/no-this': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'prefer-const': 'error'
}
};
Type System Features for FP
Advanced Type Utilities
type DeepReadonly<T> = T extends Primitive
? T
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: { readonly [K in keyof T]: DeepReadonly<T[K]> };
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type ReturnTypeOf<F> = F extends (...args: any[]) => infer R ? R : never;
type ArgumentsOf<F> = F extends (...args: infer A) => any ? A : never;
type PartialBy<T, K extends keyof T> = Omit<T, K> & <<T, K>>;
<T, K keyof T> = <T, K> & <<T, K>>;
<U> =
(U ? : )
(: infer I) => ? I : ;
<T, V> = {
[K keyof T]: T[K] V ? K : ;
}[keyof T];
<T> = [T, ...T[]];
: unique ;
<T, B> = T & { [brand]: B };
= <, >;
= <, >;
makeUserId = (: ): id ;
makeEmail = (: ): email ;
Discriminated Unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'square'; size: number };
const area = (shape: Shape): number => {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'square':
return shape.size ** 2;
}
};
type Result<E, A> =
| { readonly _tag: 'Left'; readonly left: E }
| { readonly _tag: 'Right'; readonly right: A };
= <E, A = >(: E): <E, A> =>
({ : , left });
= <A, E = >(: A): <E, A> =>
({ : , right });
matchResult = <E, A, B>(
: <E, A>,
: {
: B;
: B;
}
):
result. ===
? patterns.(result.)
: patterns.(result.);
Mapped Types
type DeepPartial<T> = T extends Primitive
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: { [K in keyof T]?: DeepPartial<T[K]> };
type DeepReadonlyMap<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
type FunctionProperties<T> = Pick<T, KeysOfType<T, Function>>;
type NonFunctionProperties<T> = Pick<T, Exclude<keyof T, KeysOfType<T, Function>>>;
type ReadonlyMethods<T> = {
readonly [K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => R
: T[K];
};
Conditional Types
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type IsExact<T, U> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2) ? true : false;
type NonNullableFields<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
type RequiredFields<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K];
};
type OptionalFields<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? never : K]: T[K];
};
Option Type Implementation
type Option<A> =
| { readonly _tag: 'None' }
| { readonly _tag: 'Some'; readonly value: A };
const None = <A = never>(): Option<A> => ({ _tag: 'None' });
const Some = <A>(value: A): Option<A> => ({ _tag: 'Some', value });
const isNone = <A>(opt: Option<A>): opt is { _tag: 'None' } =>
opt._tag === 'None';
const isSome = <A>(opt: Option<A>): opt is { _tag: 'Some'; value: A } =>
opt._tag === 'Some';
const map = <A, B>(opt: Option<A>, f: (a: A) => B): Option<B> =>
isSome(opt) ? Some(f(opt.value)) : ();
flatMap = <A, B>(: <A>, : <B>): <B> =>
(opt) ? (opt.) : ();
getOrElse = <A>(: <A>, : A):
(opt) ? opt. : defaultValue;
fold = <A, B>(
: <A>,
: B,
: B
):
(opt) ? (opt.) : ();
fromNullable = <A>(: A | | ): <A> =>
value == ? () : (value);
toNullable = <A>(: <A>): A |
(opt) ? opt. : ;
filter = <A>(: <A>, : ): <A> =>
(opt) && (opt.) ? opt : ();
pipe = <A> ({
: <B> ((opt, f)),
: <B> ((opt, f)),
: ((opt, pred)),
: (: A): (opt, defaultValue),
: <B>(: B, : B):
(opt, onNone, onSome)
});
safeDivide = (: , : ): <> =>
b === ? () : (a / b);
result = ((, ))
.( x * )
.( x > )
.();
Either Type Implementation
type Either<E, A> =
| { readonly _tag: 'Left'; readonly left: E }
| { readonly _tag: 'Right'; readonly right: A };
const Left = <E, A = never>(left: E): Either<E, A> =>
({ _tag: 'Left', left });
const Right = <A, E = never>(right: A): Either<E, A> =>
({ _tag: 'Right', right });
const isLeft = <E, A>(either: Either<E, A>): either is { _tag: 'Left'; left: E } =>
either._tag === 'Left';
const isRight = <E, A>(either: Either<E, A>): either is { _tag: 'Right'; right: A } =>
either._tag === 'Right';
const mapEither = <E, A, B>(
either: Either<E, A>,
f: (a: A) => B
): <E, B> =>
(either) ? ((either.)) : either;
mapLeft = <E, A, F>(
: <E, A>,
: F
): <F, A> =>
(either) ? ((either.)) : either;
flatMapEither = <E, A, B>(
: <E, A>,
: <E, B>
): <E, B> =>
(either) ? (either.) : either;
foldEither = <E, A, B>(
: <E, A>,
: B,
: B
):
(either) ? (either.) : (either.);
getOrElseEither = <E, A>(
: <E, A>,
: A
):
(either) ? (either.) : either.;
swap = <E, A>(: <E, A>): <A, E> =>
(either) ? (either.) : (either.);
tryCatch = <A>(
: A,
: =
e ? e : ((e))
): <, A> => {
{
(());
} (error) {
((error));
}
};
tryCatchAsync = <A>(
: <A>,
: =
e ? e : ((e))
): <<, A>> => {
{
( ());
} (error) {
((error));
}
};
pipeEither = <E, A> ({
: <B> ((either, f)),
: <F> ((either, f)),
: <B> ((either, f)),
: <B>(: B, : B):
(either, onLeft, onRight),
: (: A):
(either, onLeft),
: ((either))
});
parseJSON = <A>(: ): <, A> =>
( .(json));
validateUser = (: ): <, > => {
( data !== || data === ) {
( ());
}
(data );
};
result = (parseJSON<>(jsonString))
.(validateUser)
.( ({ ...user, : user..() }))
.(
,
);
Function Composition Utilities
function compose<A, B>(f: (a: A) => B): (a: A) => B;
function compose<A, B, C>(
f: (b: B) => C,
g: (a: A) => B
): (a: A) => C;
function compose<A, B, C, D>(
f: (c: C) => D,
g: (b: B) => C,
h: (a: A) => B
): (a: A) => D;
function compose(...fns: Function[]): Function {
return (x: any) => fns.reduceRight((acc, fn) => fn(acc), x);
}
function pipe<A>(a: A): A;
function pipe<A, B>(a: A, f: B): B;
pipe<A, B, C>(
: A,
: B,
: C
): C;
pipe<A, B, C, D>(
: A,
: B,
: C,
: D
): D;
(): {
fns.( (acc), a);
}
flow<A, B>(: B): B;
flow<A, B, C>(
: B,
: C
): C;
flow<A, B, C, D>(
: B,
: C,
: D
): D;
(): {
fns.( (acc), x);
}
double = (: ): n * ;
increment = (: ): n + ;
toString = (: ): n.();
processNumber = (double, increment, toString);
();
result = (
,
double,
increment,
toString
);
Async/Promise Utilities
type TaskEither<E, A> = () => Promise<Either<E, A>>;
const taskEitherOf = <E, A>(value: A): TaskEither<E, A> =>
async () => Right(value);
const taskEitherFromPromise = <A>(
promise: () => Promise<A>,
onError: (error: unknown) => Error
): TaskEither<Error, A> =>
async () => tryCatchAsync(promise, onError);
const mapTaskEither = <E, A, B>(
task: TaskEither<E, A>,
f: (a: A) => B
): TaskEither<E, B> =>
async () => {
const either = await task();
return mapEither(either, f);
};
const flatMapTaskEither = <E, A, B>(
task: TaskEither<E, A>,
f: (a: A) => TaskEither<E, B>
): TaskEither<E, B> =>
async () => {
const either = ();
((either)) either;
(either.)();
};
sequenceTaskEither = <E, A>(
: <E, A>[]
): <E, A[]> =>
() => {
results = .(tasks.( ()));
lefts = results.(isLeft);
(lefts. > ) lefts[];
(results.( (r ).));
};
fetchUser = (: ): <, > =>
(
().( r.()),
()
);
processUser = (
fetchUser,
(te, ({ ...user, : }))
);
Lens Pattern for Immutable Updates
type Lens<S, A> = {
get: (s: S) => A;
set: (a: A) => (s: S) => S;
};
const lens = <S, A>(
get: (s: S) => A,
set: (a: A) => (s: S) => S
): Lens<S, A> => ({ get, set });
const modify = <S, A>(
lens: Lens<S, A>,
f: (a: A) => A
): ((s: S) => S) =>
s => lens.set(f(lens.get(s)))(s);
const composeLens = <S, A, B>(
outer: Lens<S, A>,
inner: Lens<A, B>
): Lens<S, B> =>
lens(
s => inner.get(outer.get(s)),
=> outer.(inner.(b)(outer.(s)))(s)
);
prop = <S, K keyof S>(: K): <S, S[K]> =>
(
s[key],
({ ...s, [key]: value })
);
= { : ; : };
= { : ; : };
addressLens = prop<, >();
cityLens = prop<, >();
personCityLens = (addressLens, cityLens);
: = {
: ,
: { : , : }
};
updatedPerson = (personCityLens, )(person);
Validation Pattern
type Validation<E, A> = Either<readonly E[], A>;
const success = <E, A>(value: A): Validation<E, A> => Right(value);
const failure = <E, A>(errors: readonly E[]): Validation<E, A> => Left(errors);
const validateApply = <E, A, B>(
vf: Validation<E, (a: A) => B>,
va: Validation<E, A>
): Validation<E, B> => {
if (isLeft(vf) && isLeft(va)) {
return Left([...vf.left, ...va.left]);
}
if (isLeft(vf)) return vf as any;
if (isLeft(va)) return va as any;
return Right(vf.right(va.right));
};
const validate = <A>(value: A) => ({
check: <E>(
: ,
: E
): <E, A> =>
(value) ? (value) : ([error]),
: <E>(
: <>,
: E
): <<E, A>> =>
( (value)) ? (value) : ([error])
});
combineValidations = <E, A []>(
...: { [K keyof A]: <E, A[K]> }
): <E, A> => {
: E[] = [];
: [] = [];
( v validations) {
((v)) {
errors.(...v.);
} {
values.(v.);
}
}
errors. > ? (errors) : (values );
};
= { : ; : };
validateEmail = (: ): <, > =>
(email)
.(
e.(),
{ : , : }
);
validateAge = (: ): <, > =>
(age)
.(
a >= ,
{ : , : }
);
= () =>
(
(email),
(age)
);
Best Practices
1. Always use readonly for immutability
type User = {
readonly id: string;
readonly name: string;
readonly tags: readonly string[];
};
type Immutable<T> = {
readonly [K in keyof T]: T[K] extends object
? Immutable<T[K]>
: T[K];
};
2. Avoid any - use unknown or never
const parse = (json: string): any => JSON.parse(json);
const parse = (json: string): unknown => JSON.parse(json);
const parseUser = (json: string): Either<Error, User> => {
const result = tryCatch(() => JSON.parse(json));
return flatMapEither(result, data => {
if (isUser(data)) return Right(data);
return Left(new Error('Invalid user data'));
});
};
3. Use discriminated unions for state
type AsyncData<E, A> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: E }
| { status: 'success'; data: A };
const renderData = (state: AsyncData<Error, string>): string => {
switch (state.status) {
case 'idle': return 'Not started';
case 'loading': return 'Loading...';
case 'error': return state.error.message;
case 'success': return state.data;
}
};
4. Leverage type inference
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const parseNumbers = (strs: string[]): number[] =>
strs.map(s => parseInt(s, 10));
5. Use const assertions for literals
const colors = ['red', 'green', 'blue'];
const colors = ['red', 'green', 'blue'] as const;
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const;
6. Prefer type over interface for data
type Point = {
readonly x: number;
readonly y: number;
};
type Shape = Circle | Rectangle | Square;
type Readonly<T> = { readonly [K in keyof T]: T[K] };
Common Patterns
Railway-Oriented Programming
const processUser = flow(
parseJSON<unknown>,
flatMapEither(validateUser),
mapEither(normalizeUser),
flatMapEither(saveUser),
mapEither(formatResponse)
);
const result = processUser(jsonString);
Builder Pattern with Types
type Builder<T> = {
readonly build: () => T;
};
const userBuilder = () => {
let name: string | undefined;
let age: number | undefined;
return {
withName: (n: string) => (name = n, builder),
withAge: (a: number) => (age = a, builder),
build: (): Either<string, User> => {
if (!name) return Left('Name required');
if (!age) return Left('Age required');
return Right({ name, age });
}
};
};
Testing
import { describe, it, expect } from 'vitest';
describe('Option', () => {
it('maps over Some', () => {
const opt = Some(5);
const result = map(opt, n => n * 2);
expect(result).toEqual(Some(10));
});
it('maps over None', () => {
const opt = None<number>();
const result = map(opt, n => n * 2);
expect(result).toEqual(None());
});
it('flatMaps Some to Some', () => {
const opt = Some(5);
const result = flatMap(opt, n => Some(n * 2));
expect(result).toEqual(Some(10));
});
it(, {
opt = ();
result = (opt, ());
(result).(());
});
});
fc ;
(, {
(, {
fc.(
fc.(fc.(), {
opt = (n);
mapped = (opt, x);
(mapped).(opt);
})
);
});
(, {
= () => n * ;
= () => n + ;
fc.(
fc.(fc.(), {
opt = (n);
composed = ((opt, g), f);
direct = (opt, ((x)));
(composed).(direct);
})
);
});
});
Performance Optimization
const memoizeObject = <K extends object, V>(
fn: (key: K) => V
): ((key: K) => V) => {
const cache = new WeakMap<K, V>();
return (key: K): V => {
if (!cache.has(key)) {
cache.set(key, fn(key));
}
return cache.get(key)!;
};
};
const lazyMap = <A, B>(
arr: readonly A[],
f: (a: A) => B
): (() => readonly B[]) => {
let cached: readonly B[] | null = null;
return () => {
if (cached === null) {
cached = arr.map(f);
}
return cached;
};
};
const batchPromises = <A, B>(
items: readonly A[],
fn: (: A) => <B>,
:
): < B[]> => {
: A[][] = [];
( i = ; i < items.; i += batchSize) {
batches.(items.(i, i + batchSize) A[]);
}
batches.(
(acc, batch) => {
results = acc;
batchResults = .(batch.(fn));
[...results, ...batchResults];
},
.([] B[])
);
};