Help developers use functype functional programming patterns in their TypeScript projects...
Transform TypeScript code to use functype - a Scala-inspired functional programming library providing type-safe alternatives to null checks, exceptions, and imperative patterns. This skill helps integrate Option, Either, Try, List, IO, Task, and other functional types into projects.
Trigger this skill when users:
npm install functype
# or
pnpm add functype
// Import from main bundle
import { Option, Either, Left, Right, Try, List, IO, Tag, Task, Layer } from "functype"
Functype collections provide multiple ways to create instances:
| Method | Use When | Example |
|---|---|---|
List([...]) |
Creating from existing array | List(existingArray) |
List.of(...) |
Inline literal values | List.of(1, 2, 3) |
List.empty() |
Empty collections (typed) | List.empty<number>() |
Use Constructor List([...]) when:
List(data.items)List([...set])List(myArray)Use List.of(...) when:
List.of(1, 2, 3)List.of("a", "b", "c")Use List.empty() when:
List.empty<User>()// Constructor - wrapping existing data
const users = List(fetchedUsers)
const items = List([...existingSet])
// .of() - inline literals
const colors = List.of("red", "green", "blue")
const primes = Set.of(2, 3, 5, 7, 11)
// .empty() - typed empty collections
const errors = List.empty<string>()
const cache = Map.empty<string, User>()
Before (Imperative):
if (value !== null && value !== undefined) {
return value.toUpperCase()
}
return ""
After (Functype):
Option(value)
.map((v) => v.toUpperCase())
.orElse("")
Before:
const url = user?.profile?.avatar?.url
After:
const url = Option(user)
.flatMap((u) => Option(u.profile))
.flatMap((p) => Option(p.avatar))
.map((a) => a.url)
.orElse("/default-avatar.png")
Before:
try {
return JSON.parse(str)
} catch (e) {
return null
}
After (with Try):
Try(() => JSON.parse(str))
.toOption()
.orElse(null)
After (with Either):
Try(() => JSON.parse(str))
.toEither()
.fold(
(error) => `Parse failed: ${error.message}`,
(data) => data,
)
Before:
array.filter((x) => x > 0).map((x) => x * 2)
After:
List(array)
.filter((x) => x > 0)
.map((x) => x * 2)
.toArray()
Before:
if (x > 10) {
return "big"
} else if (x > 5) {
return "medium"
} else {
return "small"
}
After:
import { Cond } from "functype"
Cond.start<string>()
.case(x > 10, "big")
.case(x > 5, "medium")
.otherwise("small")
Before:
switch (status) {
case "success":
return data
case "error":
return null
default:
return undefined
}
After:
import { Match } from "functype"
Match(status)
.case("success", () => data)
.case("error", () => null)
.exhaustive()
import { Either, Left, Right } from "functype"
function validateEmail(email: string): Either<string, string> {
return email.includes("@") ? Right(email) : Left("Invalid email format")
}
function validateUser(user: any): Either<string, User> {
return validateEmail(user.email)
.map((email) => ({ ...user, email }))
.flatMap((u) => (u.age >= 18 ? Right(u) : Left("Must be 18 or older")))
}
const result = validateUser({ email: "test@example.com", age: 20 }).fold(
(error) => console.error(error),
(user) => console.log("Valid user:", user),
)
import { Option } from "functype"
interface User {
id: string
name: string
email?: string
}
function getUserEmail(userId: string): Option<string> {
return Option(fetchUser(userId))
.flatMap((user) => Option(user.email))
.filter((email) => email.includes("@"))
}
const email = getUserEmail("123").orElse("no-reply@example.com")
import { Try } from "functype"
const parseConfig = Try(() => JSON.parse(configStr))
.recover((error) => {
console.warn("Using default config:", error)
return defaultConfig
})
.map((config) => validateConfig(config))
import { List } from "functype"
const users = List([
{ name: "Alice", hobbies: ["reading", "coding"] },
{ name: "Bob", hobbies: ["gaming", "music"] },
])
const allHobbies = users
.flatMap((user) => List(user.hobbies))
.toSet() // Remove duplicates
.toArray()
IO is a lazy, composable effect type with typed errors and dependency injection:
import { IO, Tag, Layer } from "functype"
// Creation
IO.sync(() => computation()) // Sync operation
IO.succeed(value) // Pure success
IO.fail(error) // Pure failure
IO.async(() => promise) // Async operation
IO.tryPromise({
// Promise with error mapping
try: () => fetch(url),
catch: (e) => new NetworkError(e),
})
// Dependency injection
const Database = Tag<DatabaseService>("Database")
const dbEffect = IO.service(Database) // Access a service
const program = dbEffect.flatMap((db) => IO.sync(() => db.query()))
program.provide(Layer.fromValue(Database, myDb)) // Provide deps
// Generator do-notation (cleaner syntax)
const program = IO.gen(function* () {
const db = yield* IO.service(Database)
const user = yield* IO.tryPromise(() => db.findUser(id))
return user
})
// Execution
await effect.run() // Returns Promise<A>
effect.runSync() // Returns A (throws if async)
await effect.runEither() // Returns Promise<Either<E,A>>
await effect.runExit() // Returns Promise<Exit<E,A>>
// run() vs runExit(): Either has two branches, Exit has four. Use run() when all you
// need is success-or-not; use runExit() to tell a typed failure from a *defect* (a
// value that is not an E β a throwing IO.sync thunk or map/mapError callback, or
// IO.die) or from an interruption. run() puts both in the Left, raw.
const exit = await effect.runExit()
exit.isSuccess() // completed with a value
exit.isFailure() // a value from the declared E channel
exit.isDie() // a defect β NOT an E
exit.isInterrupted() // cancelled
// Both extra handlers are optional and fall back to the failure branch, so existing
// three-argument fold / three-key match calls keep working.
exit.fold(onFailure, onSuccess, onInterrupted?, onDie?)
exit.match({ Success, Failure, Interrupted, Die? })
// Defects stay recoverable: recover / recoverWith / fold / mapError treat a Die exactly
// as a Failure. Exit.Die records what the outcome was when nothing recovered it.
import { Tuple } from "functype"
const pair = Tuple(42, "hello")
pair.first() // 42
pair.second() // "hello"
pair.mapFirst((x) => x * 2) // Tuple(84, "hello")
pair.swap() // Tuple("hello", 42)
pair.apply((a, b) => a + b.length) // 47
pair.concat(Tuple(true)) // Tuple(42, "hello", true)
import { Stack } from "functype"
Stack.empty<number>()
const stack = Stack.of(1, 2, 3)
stack.push(value) // Returns new Stack
stack.pop() // Returns [Option<T>, Stack<T>]
stack.peek() // Returns Option<T>
stack.match({
Empty: () => "empty stack",
NonEmpty: (top, rest) => `top: ${top}`,
})
import { LazyList } from "functype"
// Creation
LazyList([1, 2, 3])
LazyList.of(1, 2, 3)
LazyList.empty<number>()
// Infinite sequences
const naturals = LazyList.from(0, (n) => n + 1)
const evens = naturals.filter((n) => n % 2 === 0).take(10)
// Operations are deferred until needed
const result = LazyList(hugeArray)
.filter((x) => x > 0)
.map((x) => x * 2)
.take(5)
.toArray() // Only processes first 5 matching elements
Scala-like for-comprehensions using JavaScript generators:
import { Do, DoAsync, $ } from "functype"
// Synchronous comprehensions
const result = Do(function* () {
const x = yield* $(Option(5))
const y = yield* $(Option(10))
return x + y
}) // Option(15)
// Async comprehensions
const asyncResult = await DoAsync(async function* () {
const user = yield* $(await fetchUserAsync(userId))
const profile = yield* $(await fetchProfileAsync(user.id))
return { user, profile }
})
// Cartesian products with List (2.5x-12x faster than nested flatMap)
const pairs = Do(function* () {
const x = yield* $(List([1, 2, 3]))
const y = yield* $(List([10, 20]))
return { x, y }
}) // List([{x:1,y:10}, {x:1,y:20}, {x:2,y:10}, ...])
// Mixed monad types with automatic conversion
const mixed = Do(function* () {
const a = yield* $(Option(5))
const b = yield* $(Right<string, number>(10))
return a + b
})
Note: First monad determines return type. Uses Reshapeable for automatic type conversion.
All types provide static type guards for narrowing:
import { Option, Either, Try } from "functype"
// Option type guards
if (Option.isSome(option)) {
option.value // TypeScript knows value exists
}
if (Option.isNone(option)) {
// Handle empty case
}
// Either type guards
if (Either.isRight(either)) {
either.value // TypeScript knows it's Right
}
if (Either.isLeft(either)) {
either.value // TypeScript knows it's error value
}
// Try type guards
if (Try.isSuccess(tryVal)) {
tryVal.value // TypeScript knows it succeeded
}
if (Try.isFailure(tryVal)) {
tryVal.error // TypeScript knows it failed
}
All Serializable types provide JSON, YAML, and binary serialization:
// Serialization methods
option.serialize().toJSON()
option.serialize().toYAML()
option.serialize().toBinary() // Uint8Array
// Deserialization
Option.fromJSON<string>(jsonString)
Option.fromYAML<string>(yamlString)
Option.fromBinary<string>(binaryData)
recommended errors on no-let, no-imperative-loops, prefer-map, prefer-fold, prefer-functype-map and prefer-functype-set. prefer-option, prefer-either and prefer-try warn. Write code that passes, and when code is correct because it isn't FP-shaped, declare the boundary instead of disabling the rule:
| Situation | Do this | Not this |
|---|---|---|
DB row / HTTP body / JSONB with null fields or arrays |
type UserRow = Wire<{ email: string | null; tags: ReadonlyArray<string> }> |
eslint-disable on each field |
| A collection at a boundary | Wire<ReadonlyArray<Row>> (not Wire<Row>, which is one row) |
Row[] |
A bridge whose host requires a throw / rejection / nullable (React use(), React Query, a nullableβOption converter) |
JSDoc @interop <reason> on the function |
file-level eslint-disable |
| A throw for a programmer error, not an expected failure | invariant(cond, "msg") β narrows cond after it |
throw (reported), Either for an impossible case, or a disable |
| A host needs a throw built from the failure | either.orThrow((l) => new StepError(l.message)) |
throw inside a fold |
| React state that may be empty | useState<User | null>(null) β allowed as-is |
Option inside hook state |
| A cache or registry that is mutated on purpose | native Map/Set with .set/.add β allowed as-is |
functype Map (immutable) |
| Loop that awaits sequentially and stops on failure | IO.forEach(items, f) |
.forEach with an async callback |
@interop needs a reason on the same line (@interop React Query needs a rejection.); a bare tag exempts nothing. It covers the tagged declaration and what is nested inside it. Rule of thumb: host needs the throw β @interop or orThrow(builder); only a bug can trip it β invariant(); can fail in normal operation β Either.
For a complete overview of which methods are available on each data structure, consult the Feature Matrix at:
references/feature-matrix.md (included with this skill)docs/FUNCTYPE_FEATURE_MATRIX.mdThe matrix shows which interfaces (Functor, Monad, Foldable, etc.) each type implements and what methods are available.
Option
map, flatMap, filter, foldorElse, or, orNull, orUndefined, orThrowisSome, isNone, containsEither<L, R>
map, flatMap, foldorElse, or, swapisLeft, isRightTry
map, flatMap, foldrecover, recoverWithtoOption, toEitherisSuccess, isFailuremap, flatMap, filter, reducefoldLeft, foldRightappend, prepend, concathead, tail, isEmptytoArray, toSetFor pattern conversion help, examples, and API reference:
references/feature-matrix.md for complete interface/method referencenpx functype for LLM-optimized API reference"Type 'X' is not assignable to type 'Y'"
Option<string> not Option<any>"Cannot read property 'map' of undefined"
Option(value) not just valueList([...]), Right(value)Forgetting to extract values
// Wrong - returns Option<string>
const name = Option(user).map((u) => u.name)
// Correct - returns string
const name = Option(user)
.map((u) => u.name)
.orElse("Unknown")
Using map instead of flatMap
// Wrong - returns Option<Option<string>>
Option(user).map((u) => Option(u.email))
// Correct - returns Option<string>
Option(user).flatMap((u) => Option(u.email))
Mutating instead of transforming
// Wrong - mutates original array
const list = List([1, 2, 3])
list.toArray().push(4)
// Correct - creates new List
const newList = list.append(4)
feature-matrix.md - Complete interface and method referencecommon-patterns.md - Additional pattern examples and recipesquick-reference.md - Cheat sheet for functype APIsFor more examples and detailed documentation, visit: