# Code Style

> 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…

- **Type:** Skill
- **Install:** `agentstack add skill-andrueandersoncs-claude-skill-effect-ts-code-style`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [andrueandersoncs](https://agentstack.voostack.com/s/andrueandersoncs)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [andrueandersoncs](https://github.com/andrueandersoncs)
- **Source:** https://github.com/andrueandersoncs/claude-skill-effect-ts/tree/main/skills/code-style

## Install

```sh
agentstack add skill-andrueandersoncs-claude-skill-effect-ts-code-style
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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().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().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` instead of `A | undefined`
- `Array.get` returns `Option` for safe indexing
- `Array.filterMap` combines filter and map in one pass
- `Array.partition` returns typed tuple `[excluded, satisfying]`
- `Array.groupBy` returns `Record>`
- `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  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.findFirst for search (returns Option)
const admin = Array.findFirst(users, (u) => u.role === "admin");

// ✅ REQUIRED: Array.some/every for predicates
const hasAdmin = Array.some(users, (u) => u.role === "admin");
const allVerified = Array.every(users, (u) => u.verified);

// ✅ REQUIRED: Array.filterMap for filter + transform in one pass
const validEmails = Array.filterMap(users, (u) => (isValidEmail(u.email) ? Option.some(u.email) : Option.none()));

// ✅ REQUIRED: Array.partition to split by predicate
const [minors, adults] = Array.partition(users, (u) => u.age >= 18);

// ✅ REQUIRED: Array.groupBy for grouping
const usersByRole = Array.groupBy(users, (u) => u.role);

// ✅ REQUIRED: Array.dedupe for removing duplicates
const uniqueIds = Array.dedupe(ids);

// ✅ REQUIRED: Array.match for empty vs non-empty handling
const message = Array.match(items, {
  onEmpty: () => "No items",
  onNonEmpty: (items) => `${items.length} items`,
});
```

**Record operations - use Effect's Record module:**

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

// ✅ REQUIRED: Record.map for transforming values
const doubled = Record.map(prices, (price) => price * 2);

// ✅ REQUIRED: Record.filter for filtering entries
const expensive = Record.filter(prices, (price) => price > 100);

// ✅ REQUIRED: Record.get for safe access (returns Option)
const price = Record.get(prices, "item1");

// ✅ REQUIRED: Record.keys and Record.values
const allKeys = Record.keys(config);
const allValues = Record.values(config);

// ✅ REQUIRED: Record.fromEntries and Record.toEntries
const record = Record.fromEntries([
  ["a", 1],
  ["b", 2],
]);
const entries = Record.toEntries(record);

// ✅ REQUIRED: Record.filterMap for filter + transform
const validPrices = Record.filterMap(rawPrices, (value) =>
  typeof value === "number" ? Option.some(value) : Option.none(),
);
```

**Struct operations - use Effect's Struct module:**

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

// ✅ REQUIRED: Struct.pick for selecting properties
const namePart = Struct.pick(user, "firstName", "lastName");

// ✅ REQUIRED: Struct.omit for excluding properties
const publicUser = Struct.omit(user, "password", "ssn");

// ✅ REQUIRED: Struct.evolve for transforming specific fields
const updated = Struct.evolve(user, {
  age: (age) => age + 1,
  name: (name) => name.toUpperCase(),
});

// ✅ REQUIRED: Struct.get for property access
const getName = Struct.get("name");
const name = getName(user);
```

**Tuple operations - use Effect's Tuple module:**

```typescript
import { Tuple } from "effect";

// ✅ REQUIRED: Tuple.make for creating tuples
const pair = Tuple.make("key", 42);

// ✅ REQUIRED: Tuple.getFirst/getSecond for access
const key = Tuple.getFirst(pair);
const value = Tuple.getSecond(pair);

// ✅ REQUIRED: Tuple.mapFirst/mapSecond/mapBoth for transformation
const upperKey = Tuple.mapFirst(pair, (s) => s.toUpperCase());
const doubled = Tuple.mapSecond(pair, (n) => n * 2);
const both = Tuple.mapBoth(pair, {
  onFirst: (s) => s.toUpperCase(),
  onSecond: (n) => n * 2,
});

// ✅ REQUIRED: Tuple.at for indexed access
const first = Tuple.at(tuple, 0);
```

**Predicate operations - use Effect's Predicate module:**

```typescript
import { Predicate } from "effect";

// ✅ REQUIRED: Predicate.and/or/not for combining predicates
const isPositive = (n: number) => n > 0;
const isEven = (n: number) => n % 2 === 0;
const isPositiveAndEven = Predicate.and(isPositive, isEven);
const isPositiveOrEven = Predicate.or(isPositive, isEven);
const isNegative = Predicate.not(isPositive);

// ✅ REQUIRED: Predicate.struct for validating object shapes
const isValidUser = Predicate.struct({
  name: Predicate.isString,
  age: Predicate.isNumber,
});

// ✅ REQUIRED: Predicate.tuple for validating tuple shapes
const isStringNumberPair = Predicate.tuple(Predicate.isString, Predicate.isNumber);

// ✅ REQUIRED: Built-in type guards
Predicate.isString(value);
Predicate.isNumber(value);
Predicate.isNullable(value);
Predicate.isNotNullable(value);
Predicate.isRecord(value);
```

**Effect loops - use Effect combinators:**

```typescript
// ❌ FORBIDDEN: for...of with yield*
const processAll = Effect.gen(function* () {
  const results = [];
  for (const item of items) {
    const result = yield* processItem(item);
    results.push(result);
  }
  return results;
});

// ✅ REQUIRED: Effect.forEach for sequential
const processAll = Effect.forEach(items, processItem);

// ✅ REQUIRED: Effect.all for parallel (when items are Effects)
const results = Effect.all(effects);

// ✅ REQUIRED: Effect.all with concurrency
const results = Effect.all(effects, { concurrency: 10 });

// ✅ REQUIRED: Effect.reduce for accumulation
const total = Effect.reduce(items, 0, (acc, item) => getPrice(item).pipe(Effect.map((price) => acc + price)));

// ✅ REQUIRED: Stream for large/infinite sequences
const processed = Stream.fromIterable(items).pipe(Stream.mapEffect(processItem), Stream.runCollect);
```

**Recursion for complex iteration:**

```typescript
// ❌ FORBIDDEN: while loop for tree traversal
const collectLeaves = (node) => {
  const leaves = [];
  const stack = [node];
  while (stack.length > 0) {
    const current = stack.pop();
    if (current.children.length === 0) {
      leaves.push(current);
    } else {
      stack.push(...current.children);
    }
  }
  return leaves;
};

// ✅ REQUIRED: Recursion for tree traversal
const collectLeaves = (node: TreeNode): ReadonlyArray =>
  Array.match(node.children, {
    onEmpty: () => [node],
    onNonEmpty: (children) => Array.flatMap(children, collectLeaves),
  });

// ✅ REQUIRED: Recursion with Effect
const processTree = (node: TreeNode): Effect.Effect =>
  node.children.length === 0
    ? processLeaf(node)
    : Effect.forEach(node.children, processTree).pipe(Effect.flatMap(combineResults));
```

**First-class functions - use Effect's Function module:**

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

// ❌ BAD: Inline logic repeated
const processUsers = (users: Array) => users.filter((u) => u.active).map((u) => u.email);
const processOrders = (orders: Array) => orders.filter((o) => o.active).map((o) => o.total);

// ✅ GOOD: Extract reusable predicates and transformers
const isActive = (item: T) => item.active;
const getEmail = (user: User) => user.email;
const getTotal = (order: Order) => order.total;

// ✅ GOOD: Use pipe for data transformation pipelines
const processUsers = (users: Array) => pipe(users, Array.filter(isActive), Array.map(getEmail));

const processOrders = (orders: Array) => pipe(orders, Array.filter(isActive), Array.map(getTotal));

// ✅ GOOD: Use flow to compose reusable pipelines
const getActiveEmails = flow(Array.filter(isActive), Array.map(getEmail));

const getActiveTotals = flow(Array.filter(isActive), Array.map(getTotal));

// ✅ GOOD: Use Function.compose for simple composition
const parseAndValidate = Function.compose(parse, validate);

// ✅ GOOD: Use Function.identity for pass-through
const transform = shouldTransform ? myTransform : Function.identity;

// ✅ GOOD: Use Function.constant for fixed values
const getDefaultUser = Function.constant(defaultUser);
```

**When you encounter imperative loops in existing code, refactor them immediately.** This is not optional - imperative logic is a code smell that must be eliminated.

### 1. Schema-First Data Modeling

**Define ALL data structures as Effect Schemas.** This is the foundation of type-safe Effect code.

**Key principles:**

- **Use Schema.Class over Schema.Struct** - Get methods and Schema.is() type guards
- **Use tagged unions over optional properties** - Make states explicit
- **Use Schema.is() in Match patterns** - Combine validation with matching

```typescript
import { Schema, Match } from "effect";

// ✅ GOOD: Class-based schema with methods
class User extends Schema.Class("User")({
  id: Schema.String.pipe(Schema.brand("UserId")),
  email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
  name: Schema.String.pipe(Schema.nonEmptyString()),
  createdAt: Schema.Date,
}) {
  get emailDomain() {
    return this.email.split("@")[1];
  }
}

// ✅ GOOD: Tagged union over optional properties
class Pending extends Schema.TaggedClass()("Pending", {
  orderId: Schema.String,
  items: Schema.Array(Schema.String),
}) {}

class Shipped extends Schema.TaggedClass()("Shipped", {
  orderId: Schema.String,
  items: Schema.Arr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [andrueandersoncs](https://github.com/andrueandersoncs)
- **Source:** [andrueandersoncs/claude-skill-effect-ts](https://github.com/andrueandersoncs/claude-skill-effect-ts)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-andrueandersoncs-claude-skill-effect-ts-code-style
- Seller: https://agentstack.voostack.com/s/andrueandersoncs
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
