effect-best-practices
Effect-TSにおけるサービス、エラー、レイヤー、アトムの実装パターンを強制します。`Effect.Service`、`Schema.TaggedError`、Layerの合成、またはeffect-atomを使ったReactコンポーネントを記述する際に使用してください。
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.
SKILL.md 本文
Effect-TS ベストプラクティス
このスキルは Effect-TS コードベースのための意見的で一貫したパターンを強制します。これらのパターンは型安全性、テスト可能性、可観測性、保守性を最適化します。
Effect Language Server (必須)
Effect Language Server は Effect 開発に不可欠です。 TypeScript だけでは検出できないエラーを編集時に捕捉し、Effect 固有のリファクタリングを提供し、開発者の生産性を向上させます。
セットアップ
- インストール:
npm install @effect/language-service --save-dev
tsconfig.jsonに追加:
{
"compilerOptions": {
"plugins": [{ "name": "@effect/language-service" }]
}
}
- エディタを設定してワークスペース TypeScript を使用:
- VSCode: F1 → "TypeScript: Select TypeScript Version" → "Use Workspace Version"
- JetBrains: Settings → Languages & Frameworks → TypeScript → Use workspace version
機能
- 診断: 30 以上の Effect 固有の問題を検出 (フローティング Effect、欠落した要件、不正な yield パターン)
- クイック情報: ホバーして Effect 型パラメータ (Success、Error、Requirements) を表示
- 補完:
Self、Duration 文字列、Schema ブランドの自動補完 - リファクタリング: async → Effect.gen の変換、Layer の自動合成、Schema への変換
ビルド時診断
CI での強制:
npx effect-language-service patch
設定オプションと CLI ツールについては references/language-server.md を参照してください。
クイックリファレンス: 重要なルール
| カテゴリ | すること | しないこと |
|---|---|---|
| サービス | accessors: true 付き Effect.Service | ビジネスロジックに Context.Tag |
| 依存関係 | サービス内で dependencies: [Dep.Default] | 使用箇所で手動 Layer.provide |
| レイヤー | フラット合成に Layer.mergeAll | 深くネストされた Layer.provide チェーン |
| レイヤーチェーン | インクリメンタル合成に Layer.provideMerge | 複数 Layer.provide (ネストされた型を生成) |
| エラー | message フィールド付き Schema.TaggedError | プレーンクラスまたは汎用 Error |
| エラー特異性 | UserNotFoundError, SessionExpiredError | 汎用 NotFoundError, BadRequestError |
| エラー処理 | catchTag/catchTags | catchAll または mapError |
| ID | Schema.UUID.pipe(Schema.brand("@App/EntityId")) | エンティティ ID に対する単純 string |
| 関数 | Effect.fn("Service.method") | 匿名ジェネレータ |
| ログ | 構造化データ付き Effect.log | console.log |
| 設定 | バリデーション付き Config.* | process.env を直接使用 |
| オプション | 両方のケースで Option.match | Option.getOrThrow |
| Null 可能性 | ドメイン型に Option<T> | null/undefined |
| Atom | コンポーネント外で Atom.make | レンダー内にアトムを作成 |
| Atom 状態 | グローバル状態に Atom.keepAlive | 永続状態で keepAlive を忘れる |
| Atom 更新 | React コンポーネントで useAtomSet | React から Atom.update を命令的に使用 |
| Atom クリーンアップ | サイドエフェクトに get.addFinalizer() | イベントリスナーのクリーンアップを忘れる |
| Atom 結果 | onErrorTag 付き Result.builder | ローディング/エラー状態を無視 |
サービス定義パターン
常にビジネスロジックサービスに Effect.Service を使用します。 これにより、自動アクセサ、組み込み Default レイヤー、適切な依存関係宣言が提供されます。
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 }
}),
}) {}
// 使用 - 依存関係は既にワイアリング済み
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId)
return user
})
// アプリルートで
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default)
Context.Tag が許容される場合:
- ランタイムインジェクション (Cloudflare KV、ワーカーバインディング) を伴うインフラストラクチャ
- リソースが外部から提供されるファクトリパターン
詳細なパターンについては references/service-patterns.md を参照してください。
エラー定義パターン
常にエラーに Schema.TaggedError を使用します。 これにより、シリアライズ可能になり (RPC に必須)、一貫した構造が提供されます。
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 }),
) {}
エラー処理 - catchTag/catchTags を使用:
// 正しい - 型情報を保持
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" }))
),
)
// 正しい - 複数のタグを一度に処理
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 })),
}),
)
汎用エラーより明示的なものを優先
すべての異なる失敗理由は独自のエラータイプを持つべきです。 複数の失敗モードを汎用 HTTP エラーに崩し込まないでください。
// 間違っている - 汎用エラーで情報が失われる
export class NotFoundError extends Schema.TaggedError<NotFoundError>()(
"NotFoundError",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
// その後すべてをマップ:
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" })),
})
// フロントエンドは無駄な情報を得る: { _tag: "NotFoundError", message: "Not found" }
// どのリソース? ユーザー? チャネル? メッセージ? わかりません!
// 正しい - リッチなコンテキストを持つ明示的なドメインエラー
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 }),
) {}
// フロントエンドは特定の UI を表示できます:
// - UserNotFoundError → "ユーザーが存在しません"
// - ChannelNotFoundError → "チャネルが削除されました"
// - SessionExpiredError → "セッションの有効期限が切れています。再度ログインしてください。"
エラーの再マッピングと再試行パターンについては references/error-patterns.md を参照してください。
Schema とブランド型パターン
すべてのエンティティ ID をブランドします サービス境界全体での型安全性のため:
import { Schema } from "effect"
// エンティティ ID - 常にブランド化
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>
// ドメイン型 - 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>
// ミューテーション用入力型
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>
ブランド化しない場合:
- サービス境界を越えない単純な文字列 (URL、ファイルパス)
- プリミティブ設定値
変換と高度なパターンについては references/schema-patterns.md を参照してください。
Effect.fn を使用した関数パターン
常にサービスメソッドに Effect.fn を使用します。 これにより、適切なスパン名による自動トレースが提供されます:
// 正しい - 説明的な名前を付けた Effect.fn
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
yield* Effect.annotateCurrentSpan("userId", id)
const user = yield* repo.findById(id)
return user
})
// 正しい - 複数のパラメータを持つ Effect.fn
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)
// ...
}
)
レイヤーの合成
使用箇所ではなく、サービス内に依存関係を宣言します:
// 正しい - サービス定義内の依存関係
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
// ...
}),
}) {}
// アプリルートで - シンプルなマージ
const AppLive = Layer.mergeAll(
OrderService.Default,
// インフラストラクチャレイヤー (意図的に依存関係に含まない)
DatabaseLive,
RedisLive,
)
レイヤー合成パターン:
// 同じレベルのレイヤーのフラット合成に Layer.mergeAll を使用
const RepoLive = Layer.mergeAll(
UserRepo.Default,
OrderRepo.Default,
ProductRepo.Default,
)
// インクリメンタルチェーンに Layer.provideMerge を使用 (Layer.provide より平らな型)
const MainLive = DatabaseLive.pipe(
Layer.provideMerge(ConfigServiceLive),
Layer.provideMerge(LoggerLive),
Layer.provideMerge(CacheLive),
)
レイヤーを Effect.provide より優先する理由:
- 重複排除: レイヤーは構築をメモ化します - 同じサービスは一度だけインスタンス化されます。
Effect.provideは呼び出しごとに新しいインスタンスを作成します。 - TypeScript パフォーマンス: 深い
Layer.provideネストは複雑な再帰型を生成し LSP を遅くします。Layer.mergeAllとLayer.provideMergeはより平らな型を生成します。 - リソース管理: スコープ付きレイヤーはリソースを適切に共有してクリーンアップします。
テストレイヤー、設定依存レイヤー、layerConfig パターンについては references/layer-patterns.md を参照してください。
Option の処理
Option.getOrThrow を決して使用しないでください。 常に両方のケースを明示的に処理:
// 正しい - 明示的な処理
yield* Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })),
onSome: (user) => Effect.succeed(user),
})
// 正しい - デフォルト値に getOrElse を使用
const name = Option.getOrElse(maybeName, () => "Anonymous")
// 正しい - 変換に Option.map を使用
const upperName = Option.map(maybeName, (n) => n.toUpperCase())
Effect Atom (フロントエンド状態)
Effect Atom は Effect 統合を備えた React のリアクティブ状態管理を提供します。
基本的な Atom
import { Atom } from "@effect-atom/atom-react"
// アトムはコンポーネント外で定義
const countAtom = Atom.make(0)
// 永続化すべきグローバル状態に keepAlive を使用
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive)
// エンティティごとの状態に Atom families を使用
const modalAtomFamily = Atom.family((type: string) =>
Atom.make({ isOpen: false }).pipe(Atom.keepAlive)
)
React 統合
import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom) // 読み取り専用
const setCount = useAtomSet(countAtom) // 書き込み専用
const [value, setValue] = useAtom(countAtom) // 読み取り + 書き込み
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// 値を読まずにサイドエフェクト atom をマウント
function App() {
useAtomMount(keyboardShortcutsAtom)
return <>{children}</>
}
Result.builder を使用した結果の処理
effectful な atom 結果をレンダリングするには Result.builder を使用します。 これは onErrorTag での連鎖可能なエラー処理を提供:
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>)
.onSuccess((user) => <div>Hello, {user.name}</div>)
.render()
}
サイドエフェクト付き Atom
const scrollYAtom = Atom.make((get) => {
const onScroll = () => get.setSelf(window.scrollY)
window.addEventListener("scroll", onScroll)
get.addFinalizer(() => window.removeEventListener("scroll", onScroll)) // 必須
return window.scrollY
}).pipe(Atom.keepAlive)
React ミューテーション
ミューテーション atom の場合、useState の代わりに result.waiting からローディング状態を派生させます:
const [result, mutate] = useAtom(deleteMutation, { mode: "promise" })
const isLoading = result.waiting // 自動的に更新、useState/finally 不要
ダイアログの所有権: ミューテーションロジックをダイアログコンポーネントに移動します。ダイアログはミューテーションフック、ローディング状態、トーストを所有します。親はデータプロパティと onSuccess コールバックを提供します。
キャッシュ無効化: ミューテーションとクエリアトムの両方で reactivityKeys を使用してミューテーション後のクエリを自動的に無効化 - 手動 refresh() 呼び出しを置き換えます。
完全なパターンについては references/effect-atom-patterns.md を参照してください (families、localStorage、ミューテーション、アンチパターンを含む)。
RPC とクラスターパターン
RPC コントラクトとクラスターワークフローについては、以下を参照してください:
references/rpc-cluster-patterns.md- RpcGroup、Workflow.make、Activity パターン
アンチパターン (禁止)
これらのパターンは 決して許容されません:
// 禁止 - サービス内での runSync/runPromise
const result = Effect.runSync(someEffect) // これを決してしないでください
// 禁止 - Effect.gen 内での throw
yield* Effect.gen(function* () {
if (bad) throw new Error("No!") // Effect.fail を代わりに使用
})
// 禁止 - 型情報を失う catchAll
yield* effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())))
// 禁止 - console.log
console.log("debug") // Effect.log を使用
// 禁止 - process.env を直接使用
const key = process.env.API_KEY // Config.string("API_KEY") を使用
// 禁止 - ドメイン型で null/undefined を使用
type User = { name: string | null } // Option<string> を使用
論理を伴う完全なリストについては references/anti-patterns.md を参照してください。
可観測性
// 構造化ログ
yield* Effect.log("Processing order", { orderId, userId, amount })
// メトリクス
const orderCounter = Metric.counter("orders_processed")
yield* Metric.increment(orderCounter)
// バリデーション付き設定
const config = Config.all({
port: Config.integer("PORT").pipe(Config.withDefault(3000)),
apiKey: Config.redacted("API_KEY"),
maxRetries: Config.integer("MAX_RETRIES").pipe(
Config.validate({ message: "Must be positive", validation: (n) => n > 0 })
),
})
メトリクスとトレースパターンについては references/observability-patterns.md を参照してください。
リファレンスファイル
詳細なパターンについては references/ ディレクトリのこれらのリファレンスファイルを参照してください:
language-server.md- Effect Language Service セットアップ、診断、リファクタリング、CLI ツールservice-patterns.md- サービス定義、Effect.fn、Context.Tag 例外error-patterns.md- Schema.TaggedError、エラー再マッピング、再試行パターンschema-patterns.md- ブランド型、変換、Schema.Classlayer-patterns.md- 依存関係合成、テストレイヤーrpc-cluster-patterns.md- RpcGroup、Workflow、Activity パターンeffect-atom-patterns.md- Atom、families、React フック、Result 処理anti-patterns.md- 禁止パターンの完全なリストobservability-patterns.md- ログ、メトリクス、設定パターン
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- makisuo
- リポジトリ
- makisuo/skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/makisuo/skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。