whatiskadudoing/fp-ts-skills/fp-algebraic-types

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Product types, sum types, semigroups, monoids, Eq, Ord, and building custom type class instances for domain modeling in TypeScript

Install

npx vskill@latest install whatiskadudoing/fp-ts-skills/fp-algebraic-types

SKILL.md

fp-ts Algebraic Data Types and Type Classes

This skill covers algebraic data types (ADTs) and type classes in fp-ts for robust domain modeling in TypeScript.

Core Concepts

What are Algebraic Data Types?

Algebraic Data Types (ADTs) are composite types formed by combining other types. There are two fundamental kinds:

  • Product Types: Combine types with AND (tuples, records) - "A user HAS a name AND an email"
  • Sum Types: Combine types with OR (discriminated unions) - "A result IS either success OR failure"
What are Type Classes?

Type classes define behavior that can be implemented for different types. In fp-ts, they're represented as interfaces with instances for specific types:

  • Eq: Equality comparison
  • Ord: Ordering/comparison
  • Semigroup: Combining two values of the same type
  • Monoid: Semigroup with an identity element

Product Types

Product types represent values that contain ALL of their component parts simultaneously.

Tuples

Tuples are fixed-length arrays with typed positions.

typescript
import { pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Tuple'
// Basic tuple type
type Point2D = readonly [number, number]
type NamedValue = readonly [string, number]
const point: Point2D = [10, 20]
const item: NamedValue = ['score', 100]
// Accessing tuple elements
const [x, y] = point
const [label, value] = item
// Tuple operations from fp-ts
const first = T.fst(point) // 10
const second = T.snd(point) // 20
// Map over the first element
const doubled = T.mapFst((n: number) => n * 2)(point) // [20, 20]
// Map over the second element
const incremented = T.mapSnd((n: number) => n + 1)(point) // [10, 21]
// Bimap: transform both elements
const transformed = T.bimap(
(n: number) => n.toString(),
(n: number) => n * 2
)(point) // ['20', 20]
Records (Object Types)

Records are the most common product type in TypeScript.

typescript
// Domain model as a product type
interface User {
readonly id: string
readonly email: string
readonly name: string
readonly createdAt: Date
}
// All fields must be present - this is what makes it a "product"
const user: User = {
id: '123',
email: 'john@example.com',
name: 'John Doe',
createdAt: new Date()
}
// Nested product types
interface Order {
readonly id: string
readonly customer: User
readonly items: ReadonlyArray<OrderItem>
readonly shipping: ShippingAddress
readonly billing: BillingInfo
}
interface OrderItem {
readonly productId: string
readonly quantity: number
readonly unitPrice: number
}
interface ShippingAddress {
readonly street: string
readonly city: string
readonly postalCode: string
readonly country: string
}
interface BillingInfo {
readonly method: PaymentMethod
readonly cardLast4?: string
}
When to Use Product Types

Use product types when:

  • An entity requires ALL pieces of data to be valid
  • The data naturally belongs together
  • You're modeling database entities or API payloads
  • Components are always present together
typescript
// Good: All fields are required for a valid config
interface DatabaseConfig {
readonly host: string
readonly port: number
readonly database: string
readonly username: string
readonly password: string
}
// Good: Coordinate always needs both x and y
interface Coordinate {
readonly x: number
readonly y: number
}
// Good: Money needs both amount and currency
interface Money {
readonly amount: number
readonly currency: string
}

Sum Types (Discriminated Unions)

Sum types represent values that can be ONE OF several variants.

Basic Discriminated Unions
typescript
// A payment can be ONE OF these types
type PaymentMethod =
| { readonly _tag: 'CreditCard'; readonly cardNumber: string; readonly expiry: string }
| { readonly _tag: 'PayPal'; readonly email: string }
| { readonly _tag: 'BankTransfer'; readonly accountNumber: string; readonly routingNumber: string }
| { readonly _tag: 'Crypto'; readonly walletAddress: string; readonly network: string }
// Creating values
const creditCard: PaymentMethod = {
_tag: 'CreditCard',
cardNumber: '4111111111111111',
expiry: '12/25'
}
const paypal: PaymentMethod = {
_tag: 'PayPal',
email: 'user@example.com'
}
Pattern Matching with fold/match
typescript
import { pipe } from 'fp-ts/function'
// Define a fold function for exhaustive pattern matching
const foldPaymentMethod = <R>(handlers: {
CreditCard: (cardNumber: string, expiry: string) => R
PayPal: (email: string) => R
BankTransfer: (accountNumber: string, routingNumber: string) => R
Crypto: (walletAddress: string, network: string) => R
}) => (payment: PaymentMethod): R => {
switch (payment._tag) {
case 'CreditCard':
return handlers.CreditCard(payment.cardNumber, payment.expiry)
case 'PayPal':
return handlers.PayPal(payment.email)
case 'BankTransfer':
return handlers.BankTransfer(payment.accountNumber, payment.routingNumber)
case 'Crypto':
return handlers.Crypto(payment.walletAddress, payment.network)
}
}
// Usage
const getPaymentLabel = foldPaymentMethod({
CreditCard: (num, _) => `Card ending in ${num.slice(-4)}`,
PayPal: (email) => `PayPal: ${email}`,
BankTransfer: (acc, _) => `Bank: ${acc.slice(-4)}`,
Crypto: (wallet, network) => `${network}: ${wallet.slice(0, 8)}...`
})
const label = getPaymentLabel(creditCard) // "Card ending in 1111"
Smart Constructors for Sum Types
typescript
// Define the union
type RemoteData<E, A> =
| { readonly _tag: 'NotAsked' }
| { readonly _tag: 'Loading' }
| { readonly _tag: 'Failure'; readonly error: E }
| { readonly _tag: 'Success'; readonly data: A }
// Smart constructors
const notAsked: RemoteData<never, never> = { _tag: 'NotAsked' }
const loading: RemoteData<never, never> = { _tag: 'Loading' }
const failure = <E>(error: E): RemoteData<E, never> => ({ _tag: 'Failure', error })
const success = <A>(data: A): RemoteData<never, A> => ({ _tag: 'Success', data })
// Type guards
const isNotAsked = <E, A>(rd: RemoteData<E, A>): rd is { _tag: 'NotAsked' } =>
rd._tag === 'NotAsked'
const isLoading = <E, A>(rd: RemoteData<E, A>): rd is { _tag: 'Loading' } =>
rd._tag === 'Loading'
const isFailure = <E, A>(rd: RemoteData<E, A>): rd is { _tag: 'Failure'; error: E } =>
rd._tag === 'Failure'
const isSuccess = <E, A>(rd: RemoteData<E, A>): rd is { _tag: 'Success'; data: A } =>
rd._tag === 'Success'
// Fold for exhaustive matching
const fold = <E, A, R>(handlers: {
NotAsked: () => R
Loading: () => R
Failure: (error: E) => R
Success: (data: A) => R
}) => (rd: RemoteData<E, A>): R => {
switch (rd._tag) {
case 'NotAsked': return handlers.NotAsked()
case 'Loading': return handlers.Loading()
case 'Failure': return handlers.Failure(rd.error)
case 'Success': return handlers.Success(rd.data)
}
}
// Usage in React
const UserProfile = ({ data }: { data: RemoteData<Error, User> }) =>
pipe(
data,
fold({
NotAsked: () => <button>Load Profile</button>,
Loading: () => <Spinner />,
Failure: (err) => <ErrorMessage error={err} />,
Success: (user) => <ProfileCard user={user} />
})
)
When to Use Sum Types

Use sum types when:

  • A value can be in different states
  • Different variants have different data shapes
  • You need exhaustive handling of all cases
  • Modeling state machines or workflows
typescript
// Good: Order status is mutually exclusive
type OrderStatus =
| { readonly _tag: 'Pending' }
| { readonly _tag: 'Confirmed'; readonly confirmedAt: Date }
| { readonly _tag: 'Shipped'; readonly trackingNumber: string; readonly shippedAt: Date }
| { readonly _tag: 'Delivered'; readonly deliveredAt: Date }
| { readonly _tag: 'Cancelled'; readonly reason: string; readonly cancelledAt: Date }
// Good: API response states
type ApiResponse<T> =
| { readonly _tag: 'Idle' }
| { readonly _tag: 'Loading' }
| { readonly _tag: 'Error'; readonly message: string; readonly code: number }
| { readonly _tag: 'Success'; readonly data: T }
// Good: Form field validation state
type FieldState<T> =
| { readonly _tag: 'Pristine'; readonly value: T }
| { readonly _tag: 'Valid'; readonly value: T }
| { readonly _tag: 'Invalid'; readonly value: T; readonly errors: ReadonlyArray<string> }

Semigroups

A Semigroup defines how to combine two values of the same type. It must satisfy the associativity law: combine(combine(a, b), c) === combine(a, combine(b, c))

Basic Semigroup Usage
typescript
import * as S from 'fp-ts/Semigroup'
import * as N from 'fp-ts/number'
import * as Str from 'fp-ts/string'
import { pipe } from 'fp-ts/function'
// Built-in semigroups
const sumResult = S.concatAll(N.SemigroupSum)(0)([1, 2, 3, 4]) // 10
const productResult = S.concatAll(N.SemigroupProduct)(1)([1, 2, 3, 4]) // 24
const stringResult = S.concatAll(Str.Semigroup)('')(['Hello', ' ', 'World']) // 'Hello World'
// Combining two values
const sum = N.SemigroupSum.concat(5, 3) // 8
const product = N.SemigroupProduct.concat(5, 3) // 15
Custom Semigroup Instances
typescript
import * as S from 'fp-ts/Semigroup'
import { pipe } from 'fp-ts/function'
// Semigroup for taking the first value
const first = <A>(): S.Semigroup<A> => ({
concat: (x, _) => x
})
// Semigroup for taking the last value
const last = <A>(): S.Semigroup<A> => ({
concat: (_, y) => y
})
// Semigroup for max value
const max: S.Semigroup<number> = {
concat: (x, y) => Math.max(x, y)
}
// Semigroup for min value
const min: S.Semigroup<number> = {
concat: (x, y) => Math.min(x, y)
}
Practical Example: Merging Configs
typescript
import * as S from 'fp-ts/Semigroup'
import { pipe } from 'fp-ts/function'
interface ServerConfig {
readonly host: string
readonly port: number
readonly timeout: number
readonly retries: number
readonly features: ReadonlyArray<string>
}
// Semigroup that merges configs (later values override)
const ServerConfigSemigroup: S.Semigroup<ServerConfig> = S.struct({
host: S.last<string>(), // Last value wins
port: S.last<number>(), // Last value wins
timeout: S.max(N.Ord), // Take maximum timeout
retries: S.max(N.Ord), // Take maximum retries
features: { // Concatenate arrays
concat: (x, y) => [...new Set([...x, ...y])]
}
})
// Default config
const defaultConfig: ServerConfig = {
host: 'localhost',
port: 3000,
timeout: 5000,
retries: 3,
features: ['logging']
}
// Environment-specific overrides
const prodOverrides: ServerConfig = {
host: 'api.example.com',
port: 443,
timeout: 10000,
retries: 5,
features: ['monitoring', 'caching']
}
// Merge configs
const finalConfig = ServerConfigSemigroup.concat(defaultConfig, prodOverrides)
// {
// host: 'api.example.com',
// port: 443,
// timeout: 10000,
// retries: 5,
// features: ['logging', 'monitoring', 'caching']
// }
// Merge multiple configs
const configs = [defaultConfig, devOverrides, envOverrides, cliOverrides]
const mergedConfig = S.concatAll(ServerConfigSemigroup)(defaultConfig)(configs)
Semigroup for Optional Values
typescript
import * as S from 'fp-ts/Semigroup'
import * as O from 'fp-ts/Option'
// Semigroup for Option that prefers Some values
const getOptionSemigroup = <A>(S: S.Semigroup<A>): S.Semigroup<O.Option<A>> =>
O.getMonoid(S)
// Or use the built-in
import { getApplySemigroup } from 'fp-ts/Apply'
import { Applicative } from 'fp-ts/Option'
const optionStringSemigroup = getApplySemigroup(Applicative)(Str.Semigroup)
// Example: merging optional configs
interface PartialConfig {
readonly host?: string
readonly port?: number
}
const mergePartialConfigs = (a: PartialConfig, b: PartialConfig): PartialConfig => ({
host: b.host ?? a.host,
port: b.port ?? a.port
})

Monoids

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
11
Repository forks
3
7-day trend score
12

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/whatiskadudoing/fp-ts-skills/fp-algebraic-types/badge)](https://verified-skill.com/skills/whatiskadudoing/fp-ts-skills/fp-algebraic-types)

Something wrong with this skill or its page? Tell us.