Skip to main content

effect-best-practices

Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.

معلومات المصدر

المستودع
millionco/expect
آخر نشاط في المصدر
١٧ مارس ٢٠٢٦ في ٠٢:٢٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٬٥٦٠
التفرعات
١٥٥

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
9 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
effect-best-practices
description
Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.
version
1.0.0
# Effect-TS Best Practices This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability. ## Quick Reference: Critical Rules | Category | DO | DON'T | | ----------------- | ------------------------------------------------- | ------------------------------------------ | | Services | `Effect.Service` with `accessors: true` | `Context.Tag` for business logic | | Dependencies | `dependencies: [Dep.Default]` in service | Manual `Layer.provide` at usage sites | | Errors | `Schema.TaggedError` with `message` field | Plain classes or generic Error | | Error Specificity | `UserNotFoundError`, `SessionExpiredError` | Generic `NotFoundError`, `BadRequestError` | | Error Handling | `catchTag`/`catchTags` | `catchAll` or `mapError` | | IDs | `Schema.UUID.pipe(Schema.brand("@App/EntityId"))` | Plain `string` for entity IDs | | Functions | `Effect.fn("Service.method")` | Anonymous generators | | Logging | `Effect.log` with structured data | `console.log` | | Config | `Config.*` with validation | `process.env` directly | | Options | `Option.match` with both cases | `Option.getOrThrow` | | Nullability | `Option<T>` in domain types | `null`/`undefined` | | Atoms | `Atom.make` outside components | Creating atoms inside render | | Atom State | `Atom.keepAlive` for global state | Forgetting keepAlive for persistent state | | Atom Updates | `useAtomSet` in React components | `Atom.update` imperatively from React | | Atom Cleanup | `get.addFinalizer()` for side effects | Missing cleanup for event listeners | | Atom Results | `Result.builder` with `onErrorTag` | Ignoring loading/error states | ## Service Definition Pattern **Always use `Effect.Service`** for business logic services. This provides automatic accessors, built-in `Default` layer, and proper dependency declaration. ```typescript import { Effect } from "effect"; export class UserService extends Effect.Service<UserService>()("UserService", { accessors: true, dependencies: [UserRepo.Default, CacheService.Default], effect: Effect.gen(function* () { const repo = yield* UserRepo; const cache = yield* CacheService; const findById = Effect.fn("UserService.findById")(function* (id: UserId) { const cached = yield* cache.get(id); if (Option.isSome(cached)) return cached.value; const user = yield* repo.findById(id); yield* cache.set(id, user); return user; }); const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) { const user = yield* repo.create(data); yield* Effect.log("User created", { userId: user.id }); return user; }); return { findById, create }; }), }) {} // Usage - dependencies are already wired const program = Effect.gen(function* () { const user = yield* UserService.findById(userId); return user; }); // At app root const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default); ``` **When `Context.Tag` is acceptable:** - Infrastructure with runtime injection (Cloudflare KV, worker bindings) - Factory patterns where resources are provided externally See `references/service-patterns.md` for detailed patterns. ## Error Definition Pattern **Always use `Schema.TaggedError`** for errors. This makes them serializable (required for RPC) and provides consistent structure. ```typescript import { Schema } from "effect"; import { HttpApiSchema } from "@effect/platform"; export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( "UserNotFoundError", { userId: UserId, message: Schema.String, }, HttpApiSchema.annotations({ status: 404 }), ) {} export class UserCreateError extends Schema.TaggedError<UserCreateError>()( "UserCreateError", { message: Schema.String, cause: Schema.optional(Schema.String), }, HttpApiSchema.annotations({ status: 400 }), ) {} ``` **Error handling - use `catchTag`/`catchTags`:** ```typescript // CORRECT - preserves type information yield * repo.findById(id).pipe( Effect.catchTag("DatabaseError", (err) => Effect.fail(new UserNotFoundError({ userId: id, message: "Lookup failed" })), ), Effect.catchTag("ConnectionError", (err) => Effect.fail(new ServiceUnavailableError({ message: "Database unreachable" })), ), ); // CORRECT - multiple tags at once yield * effect.pipe( Effect.catchTags({ DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })), ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })), }), ); ``` ### Prefer Explicit Over Generic Errors **Every distinct failure reason deserves its own error type.** Don't collapse multiple failure modes into generic HTTP errors. ```typescript // WRONG - Generic errors lose information export class NotFoundError extends Schema.TaggedError<NotFoundError>()( "NotFoundError", { message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} // Then mapping everything to it: Effect.catchTags({ UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })), }); // Frontend gets useless: { _tag: "NotFoundError", message: "Not found" } // Which resource? User? Channel? Message? Can't tell! ``` ```typescript // CORRECT - Explicit domain errors with rich context export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( "UserNotFoundError", { userId: UserId, message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} export class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()( "ChannelNotFoundError", { channelId: ChannelId, message: Schema.String }, HttpApiSchema.annotations({ status: 404 }), ) {} export class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()( "SessionExpiredError", { sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String, }, HttpApiSchema.annotations({ status: 401 }), ) {} // Frontend can now show specific UI: // - UserNotFoundError → "User doesn't exist" // - ChannelNotFoundError → "Channel was deleted" // - SessionExpiredError → "Your session expired. Please log in again." ``` See `references/error-patterns.md` for error remapping and retry patterns. ## Schema & Branded Types Pattern **Brand all entity IDs** for type safety across service boundaries: ```typescript import { Schema } from "effect"; // Entity IDs - always branded export const UserId = Schema.UUID.pipe(Schema.brand("@App/UserId")); export type UserId = Schema.Schema.Type<typeof UserId>; export const OrganizationId = Schema.UUID.pipe(Schema.brand("@App/OrganizationId")); export type OrganizationId = Schema.Schema.Type<typeof OrganizationId>; // Domain types - use Schema.Struct export const User = Schema.Struct({ id: UserId, email: Schema.String, name: Schema.String, organizationId: OrganizationId, createdAt: Schema.DateTimeUtc, }); export type User = Schema.Schema.Type<typeof User>; // Input types for mutations export const CreateUserInput = Schema.Struct({ email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: Schema.String.pipe(Schema.minLength(1)), organizationId: OrganizationId, }); export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>; ``` **When NOT to brand:** - Simple strings that don't cross service boundaries (URLs, file paths) - Primitive config values See `references/schema-patterns.md` for transforms and advanced patterns. ## Function Pattern with Effect.fn **Always use `Effect.fn`** for service methods. This provides automatic tracing with proper span names: ```typescript // CORRECT - Effect.fn with descriptive name const findById = Effect.fn("UserService.findById")(function* (id: UserId) { yield* Effect.annotateCurrentSpan("userId", id); const user = yield* repo.findById(id); return user; }); // CORRECT - Effect.fn with multiple parameters const transfer = Effect.fn("AccountService.transfer")(function* ( fromId: AccountId, toId: AccountId, amount: number, ) { yield* Effect.annotateCurrentSpan("fromId", fromId); yield* Effect.annotateCurrentSpan("toId", toId); yield* Effect.annotateCurrentSpan("amount", amount); // ... }); ``` ## Layer Composition **Declare dependencies in the service**, not at usage sites: ```typescript // CORRECT - dependencies in service definition export class OrderService extends Effect.Service<OrderService>()("OrderService", { accessors: true, dependencies: [UserService.Default, ProductService.Default, PaymentService.Default], effect: Effect.gen(function* () { const users = yield* UserService; const products = yield* ProductService; const payments = yield* PaymentService; // ... }), }) {} // At app root - simple merge const AppLive = Layer.mergeAll( OrderService.Default, // Infrastructure layers (intentionally not in dependencies) DatabaseLive, RedisLive, ); ``` See `references/layer-patterns.md` for testing layers and config-dependent layers. ## Option Handling **Never use `Option.getOrThrow`**. Always handle both cases explicitly: ```typescript // CORRECT - explicit handling yield * Option.match(maybeUser, { onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })), onSome: (user) => Effect.succeed(user), }); // CORRECT - with getOrElse for defaults const name = Option.getOrElse(maybeName, () => "Anonymous"); // CORRECT - Option.map for transformations const upperName = Option.map(maybeName, (n) => n.toUpperCase()); ``` ## Effect Atom (Frontend State) Effect Atom provides reactive state management for React with Effect integration. ### Basic Atoms ```typescript import { Atom } from "@effect-atom/atom-react"; // Define atoms OUTSIDE components const countAtom = Atom.make(0); // Use keepAlive for global state that should persist const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive); // Atom families for per-entity state const modalAtomFamily = Atom.family((type: string) => Atom.make({ isOpen: false }).pipe(Atom.keepAlive), ); ``` ### React Integration ```typescript import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react" function Counter() { const count = useAtomValue(countAtom) // Read only const setCount = useAtomSet(countAtom) // Write only const [value, setValue] = useAtom(countAtom) // Read + write return <button onClick={() => setCount((c) => c + 1)}>{count}</button> } // Mount side-effect atoms without reading value function App() { useAtomMount(keyboardShortcutsAtom) return <>{children}</> } ``` ### Handling Results with Result.builder **Use `Result.builder`** for rendering effectful atom results. It provides chainable error handling with `onErrorTag`: ```typescript import { Result } from "@effect-atom/atom-react" function UserProfile() { const userResult = useAtomValue(userAtom) // Result<User, Error> return Result.builder(userResult) .onInitial(() => <div>Loading...</div>) .onErrorTag("NotFoundError", () => <div>User not found</div>) .onError((error) => <div>Error: {error.message}</div>)
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub