Install
$ agentstack add skill-andrueandersoncs-claude-skill-effect-ts-code-style ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Code Style in Effect
Overview
Effect's idiomatic style centers on three core principles:
- Functional Programming Only - No imperative logic (loops, mutation, conditionals)
- Schema-First Data Modeling - Define ALL data structures as Effect Schemas
- 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 ifchains, nestedifstatements, ternary operators - Simple
if/elseis allowed: A singleifwith optionalelse(no nesting, noelse if) switch/caseas last resort: PreferMatch.type/Match.value, butswitchis 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
Arraymodule (Array.map,Array.filter,Array.reduce,Array.flatMap,Array.filterMap, etc.) - Effect's
Recordmodule (Record.map,Record.filter,Record.get,Record.keys,Record.values, etc.) - Effect's
Structmodule (Struct.pick,Struct.omit,Struct.evolve,Struct.get, etc.) - Effect's
Tuplemodule (Tuple.make,Tuple.getFirst,Tuple.mapBoth, etc.) - Effect's
Predicatemodule (Predicate.and,Predicate.or,Predicate.not,Predicate.struct, etc.) - Effect combinators (
Effect.forEach,Effect.all,Effect.reduce) - Effect's
Functionmodule (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, noelse if) else ifchains →Match.value+Match.when(FORBIDDEN aselse if)- Nested
ifstatements → Flatten with early return, orMatch.value+Match.when switch/casestatements → PreferMatch.type+Match.tag, butswitchis acceptable- Ternary operators (
? :) →Match.value+Match.whenor simpleif/else - Single optional value →
Option.match - Chained optional operations →
Option.flatMap+Option.getOrElse - Result/error conditionals →
Either.matchorEffect.match
// ✅ 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.findFirstreturnsOptioninstead ofA | undefinedArray.getreturnsOptionfor safe indexingArray.filterMapcombines filter and map in one passArray.partitionreturns typed tuple[excluded, satisfying]Array.groupByreturnsRecord>Array.matchprovides exhaustive empty/non-empty handling- All functions work with
pipefor composable pipelines - Consistent dual API (data-first and data-last)
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:
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:
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:
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:
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:
// ❌ 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:
// ❌ 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:
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
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.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.