From effect-ts
This skill should be used when the user asks about "Effect Match", "pattern matching", "Match.type", "Match.tag", "Match.when", "Schema.is()", "Schema.is with Match", "exhaustive matching", "discriminated unions", "Match.value", "converting switch to Match", "converting if/else to Match", "TaggedClass with Match", or needs to understand how Effect provides type-safe exhaustive pattern matching.
npx claudepluginhub andrueandersoncs/claude-skill-effect-ts --plugin effect-tsThis skill uses the workspace's default tool permissions.
**Pattern matching replaces complex control flow in Effect code.** Simple `if/else` (no nesting, no `else if`) is allowed, but `else if` chains, nested conditionals, and ternary operators must use pattern matching.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Pattern matching replaces complex control flow in Effect code. Simple if/else (no nesting, no else if) is allowed, but else if chains, nested conditionals, and ternary operators must use pattern matching.
Effect's Match module provides:
| Imperative Pattern | Effect Replacement |
|---|---|
Simple if/else (no nesting) | Allowed as-is |
else if chains | Match.value + Match.when |
Nested if statements | Match.value + Match.when |
switch/case statements | Prefer Match.type + Match.tag (switch acceptable) |
Ternary operators (? :) | Match.value + Match.when or simple if/else |
| Single null check | Option.match |
| Chained optionals | Option.flatMap + Option.getOrElse |
| Error checks | Either.match or Effect.match |
| Type guards | Match.when with Schema.is() |
When you encounter imperative control flow, refactor it to pattern matching immediately.
import { Match } from "effect";
const result = Match.value(input).pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive,
);
const rolePermissions = Match.type<"admin" | "user" | "guest">().pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive,
);
// Use multiple times
const perm1 = rolePermissions("admin");
const perm2 = rolePermissions("guest");
type Shape =
| { _tag: "Circle"; radius: number }
| { _tag: "Rectangle"; width: number; height: number }
| { _tag: "Triangle"; base: number; height: number };
const area = Match.type<Shape>().pipe(
Match.tag("Circle", ({ radius }) => Math.PI * radius ** 2),
Match.tag("Rectangle", ({ width, height }) => width * height),
Match.tag("Triangle", ({ base, height }) => (base * height) / 2),
Match.exhaustive,
);
area({ _tag: "Circle", radius: 5 }); // 78.54...
type AppError =
| { _tag: "NetworkError"; url: string }
| { _tag: "ValidationError"; field: string; message: string }
| { _tag: "AuthError"; reason: string };
const handleError = Match.type<AppError>().pipe(
Match.tag("NetworkError", (e) => `Failed to fetch ${e.url}`),
Match.tag("ValidationError", (e) => `${e.field}: ${e.message}`),
Match.tag("AuthError", (e) => `Auth failed: ${e.reason}`),
Match.exhaustive,
);
const describeNumber = Match.type<number>().pipe(
Match.when(
(n) => n < 0,
() => "negative",
),
Match.when(
(n) => n === 0,
() => "zero",
),
Match.when(
(n) => n > 0 && n < 10,
() => "small positive",
),
Match.when(
(n) => n >= 10,
() => "large positive",
),
Match.exhaustive,
);
const processInput = Match.type<string | number | boolean>().pipe(
Match.when(
(x): x is string => typeof x === "string",
(s) => `String: ${s.toUpperCase()}`,
),
Match.when(
(x): x is number => typeof x === "number",
(n) => `Number: ${n * 2}`,
),
Match.when(
(x): x is boolean => typeof x === "boolean",
(b) => `Boolean: ${!b}`,
),
Match.exhaustive,
);
const greet = Match.type<string>().pipe(
Match.when("morning", () => "Good morning!"),
Match.when("evening", () => "Good evening!"),
Match.orElse(() => "Hello!"),
);
greet("morning"); // "Good morning!"
greet("afternoon"); // "Hello!"
// Use when you believe all cases are covered
// Throws at runtime if unhandled case reached
const handle = Match.type<"a" | "b">().pipe(
Match.when("a", () => 1),
Match.when("b", () => 2),
Match.orElseAbsurd,
);
const classify = Match.type<number>().pipe(
Match.when(
(n) => n === 0,
() => "zero",
),
Match.not(
(n) => n > 0,
() => "negative",
), // Matches when NOT positive
Match.orElse(() => "positive"),
);
const isWeekend = Match.type<string>().pipe(
Match.whenOr("Saturday", "Sunday", () => true),
Match.orElse(() => false),
);
interface User {
role: "admin" | "user";
verified: boolean;
}
const canDelete = Match.type<User>().pipe(
Match.whenAnd(
{ role: "admin" },
(u) => u.verified,
() => true,
),
Match.orElse(() => false),
);
const processEvent = Match.type<Event>().pipe(
Match.when({ type: "click" }, (e) => handleClick(e)),
Match.when({ type: "keydown" }, (e) => handleKeydown(e)),
Match.when({ type: "submit" }, (e) => handleSubmit(e)),
Match.orElse(() => {
/* unknown event */
}),
);
interface Response {
status: number;
data: { type: string; value: unknown };
}
const handleResponse = Match.type<Response>().pipe(
Match.when({ status: 200, data: { type: "user" } }, (r) => `User: ${r.data.value}`),
Match.when({ status: 200, data: { type: "product" } }, (r) => `Product: ${r.data.value}`),
Match.when({ status: 404 }, () => "Not found"),
Match.when({ status: 500 }, () => "Server error"),
Match.orElse(() => "Unknown response"),
);
function processStatus(status: Status): string {
if (status === "pending") {
return "Waiting...";
} else if (status === "active") {
return "In progress";
} else if (status === "completed") {
return "Done!";
} else if (status === "failed") {
return "Error occurred";
} else {
return "Unknown";
}
}
const processStatus = Match.type<Status>().pipe(
Match.when("pending", () => "Waiting..."),
Match.when("active", () => "In progress"),
Match.when("completed", () => "Done!"),
Match.when("failed", () => "Error occurred"),
Match.exhaustive, // Compile error if status type changes!
);
function getDiscount(userType: UserType): number {
switch (userType) {
case "regular":
return 0;
case "premium":
return 10;
case "vip":
return 20;
default:
return 0;
}
}
const getDiscount = Match.type<UserType>().pipe(
Match.when("regular", () => 0),
Match.when("premium", () => 10),
Match.when("vip", () => 20),
Match.exhaustive,
);
const handleError = (error: AppError) =>
Match.value(error).pipe(
Match.tag("NetworkError", (e) =>
Effect.gen(function* () {
yield* Effect.logError("Network failure", { url: e.url });
return yield* Effect.fail(e);
}),
),
Match.tag("ValidationError", (e) => Effect.succeed({ field: e.field, message: e.message })),
Match.tag("AuthError", () => Effect.redirect("/login")),
Match.exhaustive,
);
Use Schema.is() in Match.when patterns to combine Schema validation with pattern matching. This works with Schema.TaggedClass and other Schema types.
Use Schema.TaggedError for domain errors - they work with Schema.is(), Effect.catchTag, and Match.tag:
Schema.is(ErrorClass) for type guards on errorsEffect.catchTag("ErrorName", ...) for error handlingMatch.tag("ErrorName", ...) when matching on errors (including predicates)import { Schema, Match } from "effect";
// Define schemas with TaggedClass for methods
class Circle extends Schema.TaggedClass<Circle>()("Circle", {
radius: Schema.Number,
}) {
get area() {
return Math.PI * this.radius ** 2;
}
get circumference() {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Schema.TaggedClass<Rectangle>()("Rectangle", {
width: Schema.Number,
height: Schema.Number,
}) {
get area() {
return this.width * this.height;
}
get perimeter() {
return 2 * (this.width + this.height);
}
}
const Shape = Schema.Union(Circle, Rectangle);
type Shape = Schema.Schema.Type<typeof Shape>;
// Schema.is() provides type guard + access to class methods
const describeShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(
Schema.is(Circle),
(c) => `Circle: area=${c.area.toFixed(2)}, circumference=${c.circumference.toFixed(2)}`,
),
Match.when(Schema.is(Rectangle), (r) => `Rectangle: area=${r.area}, perimeter=${r.perimeter}`),
Match.exhaustive,
);
// Match.tag - simpler, when you just need the data
const getShapeName = (shape: Shape) =>
Match.value(shape).pipe(
Match.tag("Circle", () => "circle"),
Match.tag("Rectangle", () => "rectangle"),
Match.exhaustive,
);
// Schema.is() - when you need class methods or type narrowing
const processShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(Schema.is(Circle), (c) => c.area), // Can use .area method
Match.when(Schema.is(Rectangle), (r) => r.area), // Can use .area method
Match.exhaustive,
);
// Schema.is() also works for runtime validation of unknown data
const handleUnknown = (input: unknown) =>
Match.value(input).pipe(
Match.when(Schema.is(Circle), (c) => `Valid circle with radius ${c.radius}`),
Match.when(Schema.is(Rectangle), (r) => `Valid rectangle ${r.width}x${r.height}`),
Match.orElse(() => "Invalid shape"),
);
// Or use for type narrowing
const processInput = (input: unknown) => {
if (Schema.is(Circle)(input)) {
console.log(`Circle area: ${input.area}`); // Type is Circle, has methods
}
};
import { Schema, Match, Effect } from "effect";
// Define states with TaggedClass
class Draft extends Schema.TaggedClass<Draft>()("Draft", {
content: Schema.String,
}) {
get isEmpty() {
return this.content.trim().length === 0;
}
}
class Published extends Schema.TaggedClass<Published>()("Published", {
content: Schema.String,
publishedAt: Schema.Date,
}) {
get daysSincePublish() {
return Math.floor((Date.now() - this.publishedAt.getTime()) / 86400000);
}
}
class Archived extends Schema.TaggedClass<Archived>()("Archived", {
content: Schema.String,
archivedReason: Schema.String,
}) {}
const Article = Schema.Union(Draft, Published, Archived);
type Article = Schema.Schema.Type<typeof Article>;
// Process with Schema.is() to access class methods
const getArticleStatus = (article: Article) =>
Match.value(article).pipe(
Match.when(Schema.is(Draft), (d) => (d.isEmpty ? "Empty draft" : "Draft with content")),
Match.when(Schema.is(Published), (p) => `Published ${p.daysSincePublish} days ago`),
Match.when(Schema.is(Archived), (a) => `Archived: ${a.archivedReason}`),
Match.exhaustive,
);
Option.match is for single Option-to-value conversion. For chaining multiple optional operations, use Option.flatMap:
// ✅ GOOD: Single Option.match (converting Option to different type)
const greeting = Option.match(maybeUser, {
onNone: () => "Hello, guest!",
onSome: (user) => `Hello, ${user.name}!`,
});
// ❌ BAD: Nested Option.match (every onNone returns same default)
const result = Option.match(maybeA, {
onNone: () => fallback,
onSome: (a) =>
Option.match(maybeB(a), {
onNone: () => fallback,
onSome: (b) => transform(b),
}),
});
// ✅ GOOD: Option.flatMap chain (flat, readable, single fallback)
const result = pipe(
maybeA,
Option.flatMap(maybeB),
Option.map(transform),
Option.getOrElse(() => fallback),
);
Rule: When every onNone branch returns the same value, that's a signal to flatten with Option.flatMap + Option.getOrElse.
else if - Replace with Match.value + Match.whenif statements - Flatten or replace with Matchswitch/case - But switch is acceptable as last resortif (x != null) - Replace with Option.match._tag directly - Replace with Match.tag or Schema.is()Option.flatMap chains with Option.getOrElse when all onNone branches share the same fallbackFor comprehensive pattern matching documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections: