Claude
Skills
Sign in
Back

code-style

Included with Lifetime
$97 forever

This skill should be used EVERY TIME you're writing TypeScript with Effect, especially when the user asks about "Effect best practices", "Effect code style", "idiomatic Effect", "functional programming", "no loops", "no for loops", "avoid imperative", "Effect Array", "Effect Record", "Effect Struct", "Effect Tuple", "Effect Predicate", "Schema-first", "Match-first", "when to use Schema", "when to use Match", "branded types", "dual APIs", "Effect guidelines", "do notation", "Effect.gen", "pipe vs method chaining", "Effect naming conventions", "Effect project structure", "data modeling in Effect", or needs to understand idiomatic Effect-TS patterns and conventions.

Writing & Docs

What this skill does


# Code Style in Effect

## Overview

Effect's idiomatic style centers on three core principles:

1. **Functional Programming Only** - No imperative logic (loops, mutation, conditionals)
2. **Schema-First Data Modeling** - Define ALL data structures as Effect Schemas
3. **Match-First Control Flow** - Define ALL conditional logic using Effect Match

Additional patterns include:

- **Branded types** - Nominal typing for primitives (built into Schema)
- **Dual APIs** - Both data-first and data-last
- **Generator syntax** - Effect.gen for readability
- **Project organization** - Layers, services, domains

## Core Principles

### 0. No Imperative Logic - Functional Programming Only

**NEVER use imperative constructs.** All code must follow functional programming principles:

- **No complex conditionals**: `else if` chains, nested `if` statements, ternary operators
- **Simple `if/else` is allowed**: A single `if` with optional `else` (no nesting, no `else if`)
- **`switch/case` as last resort**: Prefer `Match.type`/`Match.value`, but `switch` is acceptable when Match doesn't fit
- **No loops**: `for`, `while`, `do...while`, `for...of`, `for...in`
- **No mutation**: Reassignment, push/pop/splice, property mutation

**Use instead:**

- Pattern matching (`Match`, `Option.match`, `Either.match`, `Array.match`)
- Effect's `Array` module (`Array.map`, `Array.filter`, `Array.reduce`, `Array.flatMap`, `Array.filterMap`, etc.)
- Effect's `Record` module (`Record.map`, `Record.filter`, `Record.get`, `Record.keys`, `Record.values`, etc.)
- Effect's `Struct` module (`Struct.pick`, `Struct.omit`, `Struct.evolve`, `Struct.get`, etc.)
- Effect's `Tuple` module (`Tuple.make`, `Tuple.getFirst`, `Tuple.mapBoth`, etc.)
- Effect's `Predicate` module (`Predicate.and`, `Predicate.or`, `Predicate.not`, `Predicate.struct`, etc.)
- Effect combinators (`Effect.forEach`, `Effect.all`, `Effect.reduce`)
- Effect's `Function` module (`pipe`, `flow`, `identity`, `constant`, `compose`)
- Recursion for complex iteration
- First-class functions (pass functions as arguments, return functions)

#### Conditionals - Use Pattern Matching

- **Simple `if/else`** → Allowed (no nesting, no `else if`)
- **`else if` chains** → `Match.value` + `Match.when` (FORBIDDEN as `else if`)
- **Nested `if` statements** → Flatten with early return, or `Match.value` + `Match.when`
- **`switch/case` statements** → Prefer `Match.type` + `Match.tag`, but `switch` is acceptable
- **Ternary operators (`? :`)** → `Match.value` + `Match.when` or simple `if/else`
- **Single optional value** → `Option.match`
- **Chained optional operations** → `Option.flatMap` + `Option.getOrElse`
- **Result/error conditionals** → `Either.match` or `Effect.match`

```typescript
// ✅ ALLOWED: Simple if/else (not nested, no else if)
if (user.isAdmin) {
  return grantFullAccess()
}
return grantLimitedAccess()

// ✅ ALLOWED: Simple if with else
if (isValid) {
  process(data)
} else {
  handleError()
}

// ❌ FORBIDDEN: else if (use Match instead)
if (user.role === "admin") {
  return "full access"
} else if (user.role === "user") {
  return "limited access"
} else {
  return "no access"
}

// ❌ FORBIDDEN: Nested if
if (user.isActive) {
  if (user.isAdmin) {
    return "active admin"
  }
}

// ❌ FORBIDDEN: ternary
const message = isError ? "Failed" : "Success"

// ❌ FORBIDDEN: direct ._tag access
if (event._tag === "UserCreated") { ... }
const isCreated = event._tag === "UserCreated"

// ❌ FORBIDDEN: ._tag in type definitions
type ConflictTag = Conflict["_tag"]  // Never extract _tag as a type

// ❌ FORBIDDEN: ._tag in array predicates
const hasConflict = conflicts.some((c) => c._tag === "MergeConflict")
const mergeConflicts = conflicts.filter((c) => c._tag === "MergeConflict")
const countMerge = conflicts.filter((c) => c._tag === "MergeConflict").length

// ✅ REQUIRED: Schema.is() as predicate
const hasConflict = conflicts.some(Schema.is(MergeConflict))
const mergeConflicts = conflicts.filter(Schema.is(MergeConflict))
const countMerge = conflicts.filter(Schema.is(MergeConflict)).length

// ✅ REQUIRED: Match.value for else-if replacement
const getAccess = (user: User) =>
  Match.value(user.role).pipe(
    Match.when("admin", () => "full access"),
    Match.when("user", () => "limited access"),
    Match.orElse(() => "no access")
  )

// ✅ REQUIRED: Match.type for multi-case
const getStatusMessage = Match.type<Status>().pipe(
  Match.when("pending", () => "waiting"),
  Match.when("active", () => "running"),
  Match.exhaustive
)

// ✅ ALLOWED: switch as last resort (prefer Match)
switch (status) {
  case "pending": return "waiting"
  case "active": return "running"
  default: return "unknown"
}

// ✅ REQUIRED: Option.match for single optional
const displayName = Option.match(maybeUser, {
  onNone: () => "Guest",
  onSome: (user) => user.name
})

// ❌ FORBIDDEN: Nested Option.match (pyramid of doom)
// Signal: every onNone returns the same default
const name = pipe(
  findUser(id),
  Option.match({
    onNone: () => "Unknown",
    onSome: (user) =>
      Option.match(user.profile, {
        onNone: () => "Unknown",
        onSome: (profile) => profile.displayName,
      }),
  }),
)

// ✅ REQUIRED: Option.flatMap chain for multiple optionals
const name = pipe(
  findUser(id),
  Option.flatMap((user) => user.profile),
  Option.map((profile) => profile.displayName),
  Option.getOrElse(() => "Unknown"),
)

// ✅ REQUIRED: Either.match for results
const result = Either.match(parseResult, {
  onLeft: (error) => `Error: ${error}`,
  onRight: (value) => `Success: ${value}`
})

// ✅ REQUIRED: Match.tag for discriminated unions (not ._tag access)
const handleEvent = Match.type<AppEvent>().pipe(
  Match.tag("UserCreated", (e) => notifyAdmin(e.userId)),
  Match.tag("UserDeleted", (e) => cleanupData(e.userId)),
  Match.exhaustive
)

// ✅ REQUIRED: Schema.is() for type guards on Schema types (Schema.TaggedClass)
if (Schema.is(UserCreated)(event)) {
  // event is narrowed to UserCreated
}

// Schema.TaggedError works with Schema.is(), Effect.catchTag, and Match.tag.
// Always use Schema.TaggedError for domain errors.
```

**When you encounter `else if` chains, nested `if` statements, or ternary operators in existing code, refactor them immediately.** Simple `if/else` is acceptable.

#### Loops - Use Effect's Array Module and Recursion

**NEVER use `for`, `while`, `do...while`, `for...of`, or `for...in` loops.** Use Effect's functional alternatives.

**Why Effect's Array module over native Array methods:**

- `Array.findFirst` returns `Option<A>` instead of `A | undefined`
- `Array.get` returns `Option<A>` for safe indexing
- `Array.filterMap` combines filter and map in one pass
- `Array.partition` returns typed tuple `[excluded, satisfying]`
- `Array.groupBy` returns `Record<K, NonEmptyArray<A>>`
- `Array.match` provides exhaustive empty/non-empty handling
- All functions work with `pipe` for composable pipelines
- Consistent dual API (data-first and data-last)

```typescript
import { Array, pipe } from "effect";

// ❌ FORBIDDEN: for loop
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

// ❌ FORBIDDEN: for...of loop
const results = [];
for (const item of items) {
  results.push(process(item));
}

// ❌ FORBIDDEN: while loop
let sum = 0;
let i = 0;
while (i < numbers.length) {
  sum += numbers[i];
  i++;
}

// ❌ FORBIDDEN: forEach with mutation
const output = [];
items.forEach((item) => output.push(transform(item)));

// ✅ REQUIRED: Array.map for transformation
const doubled = Array.map(numbers, (n) => n * 2);
// or with pipe
const doubled = pipe(
  numbers,
  Array.map((n) => n * 2),
);

// ✅ REQUIRED: Array.filter for selection
const adults = Array.filter(users, (u) => u.age >= 18);

// ✅ REQUIRED: Array.reduce for accumulation
const sum = Array.reduce(numbers, 0, (acc, n) => acc + n);

// ✅ REQUIRED: Array.flatMap for one-to-many
const allTags = Array.flatMap(posts, (post) => post.tags);

// ✅ REQUIRED: Array.f

Related in Writing & Docs