Claude Code for Remeda: Type-Safe Utility Library for TypeScript — Claude Skills 360 Blog
Blog / Tooling / Claude Code for Remeda: Type-Safe Utility Library for TypeScript
Tooling

Claude Code for Remeda: Type-Safe Utility Library for TypeScript

Published: March 16, 2027
Read time: 7 min read
By: Claude Skills 360

Remeda is a TypeScript-first utility library with full type inference — every function is typed so the output type matches the transformation. pipe(data, fn1, fn2, fn3) composes data-first transformations while preserving types through each step. Functions are “purried”: they work data-first (map(array, fn)) or data-last (map(fn)(array)) for pipe composition. groupBy, sortBy, flatMap, uniq, zip, and chunk all infer output types. pick, omit, mapValues, mergeDeep, and mapKeys transform objects type-safely. isDefined, isString, isNonNullish, and isArray are type guard predicates usable as filter callbacks. createLazyInvocable enables lazy evaluation chains that only execute when consumed. Claude Code generates Remeda data transformation pipelines, groupBy+mapValues aggregations, type-safe filter chains, and object manipulation utilities for TypeScript applications.

CLAUDE.md for Remeda

## Remeda Stack
- Version: remeda >= 2.0
- Pipe: pipe(data, R.map(fn), R.filter(pred), R.sortBy([key, "asc"]))
- Group: R.groupBy(orders, o => o.status) — Record<Status, Order[]>
- Object: R.pick(obj, ["id", "name"]) / R.omit(obj, ["password"]) / R.mapValues(obj, fn)
- Guards: R.isDefined / R.isNonNullish / R.isString — use as filter(R.isDefined) predicates
- Sort: R.sortBy([key, "desc"], [key2, "asc"]) — multi-key sort
- Merge: R.mergeDeep(defaults, overrides) — deep merge preserving types
- Chunk: R.chunk(array, size) — split into batches

Data Transformation Pipelines

// lib/order-transforms.ts — Remeda data pipelines
import * as R from "remeda"

interface Order {
  id: string
  customerId: string
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
  totalCents: number
  items: OrderItem[]
  createdAt: Date
  region: "us" | "eu" | "apac"
}

interface OrderItem {
  productId: string
  name: string
  priceCents: number
  quantity: number
}

// Group orders by status with type inference
export function groupOrdersByStatus(orders: Order[]) {
  return R.groupBy(orders, o => o.status)
  // Returns: Partial<Record<Order["status"], Order[]>>
}

// Aggregate revenue by region
export function revenueByRegion(orders: Order[]) {
  return R.pipe(
    orders,
    R.filter(o => o.status !== "cancelled"),
    R.groupBy(o => o.region),
    R.mapValues(regionOrders =>
      R.pipe(
        regionOrders,
        R.map(o => o.totalCents),
        R.sum()
      )
    )
  )
  // Returns: Partial<Record<"us" | "eu" | "apac", number>>
}

// Top products by revenue across all orders
export function topProductsByRevenue(orders: Order[], limit = 10) {
  return R.pipe(
    orders,
    R.flatMap(o => o.items),
    R.groupBy(item => item.productId),
    R.mapValues(items => ({
      name: items[0]!.name,
      totalRevenue: R.pipe(
        items,
        R.map(i => i.priceCents * i.quantity),
        R.sum()
      ),
      totalSold: R.sumBy(items, i => i.quantity),
    })),
    R.values(),
    R.sortBy([v => v.totalRevenue, "desc"]),
    R.take(limit)
  )
}

// Orders summary with stats
export function ordersSummary(orders: Order[]) {
  const activeOrders = R.filter(orders, o => o.status !== "cancelled")

  return {
    total: orders.length,
    active: activeOrders.length,
    cancelRate: orders.length > 0
      ? (orders.length - activeOrders.length) / orders.length
      : 0,
    avgOrderValue: activeOrders.length > 0
      ? R.sumBy(activeOrders, o => o.totalCents) / activeOrders.length
      : 0,
    byStatus: R.mapValues(
      R.groupBy(orders, o => o.status),
      group => group.length
    ),
  }
}

Type Guard Predicates

// lib/data-utils.ts — type guards and filtering
import * as R from "remeda"

// Filter undefined from arrays — type narrows to T[]
export function compact<T>(arr: (T | undefined | null)[]): T[] {
  return R.filter(arr, R.isNonNullish)
}

// Safe first/last with explicit undefined
export function safeHead<T>(arr: T[]): T | undefined {
  return R.first(arr)  // undefined if empty, typed T | undefined
}

// Parse and filter valid orders from API response
interface ApiOrder {
  id?: string
  status?: string
  totalCents?: number
}

interface ValidOrder {
  id: string
  status: string
  totalCents: number
}

export function parseOrders(raw: ApiOrder[]): ValidOrder[] {
  return R.pipe(
    raw,
    R.filter((o): o is ValidOrder =>
      R.isString(o.id) &&
      R.isString(o.status) &&
      R.isNumber(o.totalCents)
    )
  )
}

// Unique values with type inference
export function uniqueStatuses(orders: { status: string }[]): string[] {
  return R.pipe(
    orders,
    R.map(o => o.status),
    R.unique()
  )
}

// Partition by predicate
export function partitionByStatus(orders: { status: string; id: string }[]) {
  const [active, cancelled] = R.partition(
    orders,
    o => o.status !== "cancelled"
  )
  return { active, cancelled }
}

Object Utilities

// lib/object-utils.ts — pick, omit, mergeDeep
import * as R from "remeda"

interface UserProfile {
  id: string
  email: string
  name: string
  passwordHash: string
  role: "admin" | "user"
  preferences: {
    theme: "light" | "dark"
    notifications: boolean
    emailFrequency: "daily" | "weekly" | "never"
  }
  createdAt: Date
}

// Safe public projection — omit sensitive fields
export function toPublicProfile(user: UserProfile) {
  return R.omit(user, ["passwordHash", "createdAt"])
  // Returns: Omit<UserProfile, "passwordHash" | "createdAt">
}

// Pick only needed fields for API response
export function toUserSummary(user: UserProfile) {
  return R.pick(user, ["id", "name", "role"])
  // Returns: Pick<UserProfile, "id" | "name" | "role">
}

// Deep merge defaults with user preferences
const DEFAULT_PREFERENCES: UserProfile["preferences"] = {
  theme: "light",
  notifications: true,
  emailFrequency: "weekly",
}

export function applyPreferenceDefaults(
  partial: Partial<UserProfile["preferences"]>
): UserProfile["preferences"] {
  return R.mergeDeep(DEFAULT_PREFERENCES, partial)
}

// Transform object values
export function centsToDollars(prices: Record<string, number>): Record<string, number> {
  return R.mapValues(prices, cents => cents / 100)
}

// Rename keys
export function toSnakeCase<T>(obj: Record<string, T>): Record<string, T> {
  return R.mapKeys(obj, key =>
    key.replace(/([A-Z])/g, "_$1").toLowerCase()
  )
}

Sorting and Chunking

// lib/batch-utils.ts — sort, chunk, zip
import * as R from "remeda"

interface Product {
  id: string
  name: string
  priceCents: number
  stock: number
  category: string
  rating: number
}

// Multi-key sort: category asc, price desc
export function sortProducts(products: Product[]) {
  return R.sortBy(
    products,
    [p => p.category, "asc"],
    [p => p.priceCents, "desc"],
    [p => p.rating, "desc"]
  )
}

// Process in batches (for API rate limiting)
export async function processBatched<T, R>(
  items: T[],
  batchSize: number,
  processor: (batch: T[]) => Promise<R[]>
): Promise<R[]> {
  const batches = R.chunk(items, batchSize)
  const results = await Promise.all(batches.map(processor))
  return R.flat(results)
}

// Zip two arrays into tuples
export function zipWithIndex<T>(items: T[]): [T, number][] {
  return R.zip(items, R.range(0, items.length))
}

// Find duplicates
export function findDuplicateIds(orders: { id: string }[]): string[] {
  return R.pipe(
    orders,
    R.map(o => o.id),
    R.groupBy(id => id),
    R.entries(),
    R.filter(([, group]) => group.length > 1),
    R.map(([id]) => id)
  )
}

// Rolling window aggregation
export function rollingAverage(values: number[], window: number): number[] {
  return R.pipe(
    R.range(window - 1, values.length),
    R.map(i => {
      const slice = values.slice(i - window + 1, i + 1)
      return R.sum(slice) / window
    })
  )
}

For the Lodash alternative when utility functions for deep cloning (_.cloneDeep), debouncing (_.debounce), and template strings are needed alongside collection transforms — Lodash has a larger API but weaker TypeScript types than Remeda, see the utility library comparison. For the fp-ts alternative when algebraic data types (Option, Either, TaskEither) and full Haskell-style functional programming composition are needed beyond Remeda’s utilities — fp-ts adds monadic chaining but requires steeper learning curve, see the functional programming guide. The Claude Skills 360 bundle includes Remeda skill sets covering pipe composition, groupBy aggregations, and type-safe transformations. Start with the free tier to try Remeda pipeline generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free