effect-best-practices について
effect-best-practices は、Effect を使う TypeScript チーム向けのコーディングスキルです。サービス、エラー、合成、スキーマ、リソースのライフサイクルに関する規約と診断手順をまとめています。
主な機能
- 01Effect サービスと依存関係の指針
- 02型付きエラーの定義と処理パターン
- 03スキーマとブランド ID の規約
- 04Effect.fn と合成の指針
- 05コマンドラインでの言語サーバー診断
- 06リソースのスコープとクリーンアップ
活用シーン
- 01プロジェクト規約に沿って Effect コードを確認
- 02TypeScript の編集後に診断を実行
- 03型付きエラーとサービスのパターンを選定
- 04Layer を合成してリソースを管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ forcedotcom/ salesforcedx-vscode --skill effect-best-practicesインストール後、Claude Code で/effect-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdcomposition-style.mddiagnostics-findings.mderror-patterns.mdlayer-patterns.mdobservability-patterns.mdschema-patterns.mdservice-patterns.md詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
これらのパターンに対する diff/plan レビューには、effect-advocate サブエージェント(.claude/agents/effect-advocate.md)を呼び出してください。
Effect LS 診断(エージェントでの使用)
Cursor の read_lints は Effect Language Server の診断を表示しません。CLI を使用してください:
npx effect-language-service diagnostics --file <path>
# or whole project:
npx effect-language-service diagnostics --project tsconfig.json
- PostToolUse の
verify-on-edit.shフックは、.tsの Edit/Write ごとに--file <edited>を自動実行し、出力をfollowup_messageとして表示します。報告された内容に対応してください。 - エラーだけでなく、警告とメッセージにも対応してください。
references/diagnostics-findings.mdには、よくある指摘とその修正方法が対応付けられています。config/effect-diagnostics.jsonのenforcedRulesがビルドゲートです。 - ルールを強制するには、
enforcedRulesへのエントリだけでなく 2 つの編集が必要です。詳細はreferences/diagnostics-findings.mdを参照してください。 - 編集を一通り行った後は、影響を受けるパッケージに対して
--project tsconfig.jsonを実行し、ファイルをまたぐ問題を検出してください。 effect-language-service quickfixesは提案されたコード変更を表示します。
クイックリファレンス:重要なルール
| カテゴリ | DO | DON'T |
|---|---|---|
| サービス | accessors: true を指定した Effect.Service | ビジネスロジックに Context.Tag を使う |
| 依存関係 | サービス内で dependencies: [Dep.Default] を指定 | 使用箇所で手動の Layer.provide を使う |
| エラー | message フィールドを持つ Schema.TaggedError | 通常のクラスや汎用的な Error を使う |
| エラーの具体性 | catch の分岐やフィールドの形状が異なる場合はタグを分ける。テレメトリの次元は 1 つのタグのフィールドにする | どれも message を出力したり、同じスパンを発生させたりする余分なタグ |
| エラー処理 | catchTag/catchTags を使い、必要な場合にのみ catch する | catchAll、握りつぶし、「念のため」の catch を使う |
| ID | Salesforce のレコード/組織には SalesforceId/OrgId(core/schemas/salesforceId.ts)を使用する。DefaultOrgInfoSchema.orgId/devHubOrgId には cliId と同様に Schema.optional(OrgId) を使用する。それ以外には Schema.UUID.pipe(Schema.brand("@App/EntityId")) を使用する | 通常の string、場当たり的な getAuthInfoFields().orgId、DefaultOrgInfo の Option としての optionalWith |
| 関数 | Effect.gen より Effect.fn を使う。.gen は共有パイプにのみ使う | 無名ジェネレーター、リカバリーを追加するためのネストした Effect.gen、ビジネスロジックでの .gen |
| 合成 | .pipe を使い、const は 2 回以上読み取る場合にのみ使う。詳細:references/composition-style.md | 1 回しか使わない const x = yield* を定義してから f(x) を呼び出す |
| パラメーターと依存関係 | パラメーター=実行時データ、依存関係=コンテキストから yield するもの | Ref/PubSub/サービスをパラメーターとして渡す |
| 命名 | コマンドには FooCommand、ヘルパーにはドメイン名を使う | FooEffect のサフィックスを使う(冗長です。TS/Effect.fn ですでに型が伝わります) |
| ロギング | 構造化データとともに Effect.log を使う | console.log を使う |
| 設定 | 検証を伴う Config.* を使う | process.env を直接使う(ESBUILD_* のようなビルド時変数を除く) |
| 時間の値 | Duration.seconds(30)、Duration.millis(5000) を使い、パラメーターには Duration.DurationInput を使う | ミリ秒の数値を number パラメーターとして使う、または TIMEOUT_MS = 30_000 定数を使う |
| Option | 両方のケースを指定した Option.match を使う | Option.getOrThrow を使う |
| Null 許容性 | ドメイン型で Option<T> を使う | null/undefined を使う |
| Atom | コンポーネントの外で Atom.make を使う | レンダリング中に Atom を作成する |
| Atom の状態 | グローバル状態には Atom.keepAlive を使う | 永続状態で keepAlive を付け忘れる |
| Atom の更新 | React コンポーネントで useAtomSet を使う | React から命令的に Atom.update を使う |
| Atom のクリーンアップ | 副作用には get.addFinalizer() を使う | イベントリスナーのクリーンアップを忘れる |
| リソースのクリーンアップ | Scoped service/layer と Effect.addFinalizer を使う | dispose を返す、Effect が所有するリソースの管理を呼び出し元に委ねる |
| Atom の結果 | onErrorTag を指定した Result.builder を使う | ローディング状態/エラー状態を無視する |
| グループ化 | Arr.groupBy(effect/Array)を使う | Partial<Record> のためにフィルターが必要になる Object.groupBy |
サービス定義パターン
ビジネスロジックのサービスには、常に 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 };
})
}) {}
// Usage - dependencies are already wired
const program = UserService.findById(userId);
// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);
Context.Tag が許容される場合:
- 実行時に注入するインフラストラクチャ(Cloudflare KV、ワーカーバインディング)
- リソースを外部から提供するファクトリパターン
- 呼び出し元が実装を提供するインターフェイス —
.Defaultとしてまとめられる単一の標準実装がないもの(例:VS Code ホストとテスト用の偽物によってそれぞれ一度ずつ実装されるSoqlBuilderService)。詳しくはreferences/service-patterns.mdを参照してください
Params と Dependencies
- Params = 呼び出しごとの実行時データ(ID、ユーザー入力、呼び出しごとの設定)
- Dependencies = 共有インフラストラクチャ(Ref、PubSub、SubscriptionRef、サービス)。レイヤー経由で提供し、エフェクトの内部で yield してください
- Ref/PubSub などはレイヤー内で構築してください(例:
buildAllServicesLayer)。利用側では yield し、Params として受け取らないでください
// WRONG - passing shared infra as params
const createStatusBar = (pubsub: PubSub.PubSub<void>, stateRef: SubscriptionRef.SubscriptionRef<State>) =>
Effect.gen(...)
// Caller must create and pass; wiring scattered at call sites
// CORRECT - yield inside, build in layer
const PubSubTag = Context.GenericTag<PubSub.PubSub<void>>("PubSub")
const createStatusBar = Effect.gen(function* () {
const pubsub = yield* PubSubTag
const stateRef = yield* StateRefTag
// ...
})
// Layer: Layer.effect(PubSubTag, PubSub.sliding<void>(1))
詳しいパターンについては 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 を使用してください:
// 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 }))
})
);
キャッチする場合と、しない場合
ほとんどのエラーはユーザーに表示されます(実行時のメッセージ/トースト)。次の場合にのみキャッチしてください。
- 本当に無視する場合 — 失敗を許容して処理を続ける(例:任意の事前作成)
- より良いメッセージを表示する場合 — デフォルトのメッセージが曖昧なため、より明確なドメインエラーにマッピングする
想定内のスキップ(任意のプラグインが見つからない、互換性のないバージョン)の場合は、成功パス(ガード/Match 分岐)で nls を書き込んでください。fail+catchTag は、予期しない事態からの復旧に使用します。「この文字列を出力する」だけのハンドラーには使いません。
キャッチは控えめにしてください。catchAll や「安全のために握りつぶす」処理は使わないでください。catchTag/catchTags を使用し、ログに記録するか、改善したエラーで失敗させてください。
汎用的なエラーより明示的なエラーを優先する
catch の分岐やフィールドの形状が異なる場合はタグを分けてください。たとえば、message+cause と message+cause+setting の違いです。同じ catch 処理(message の出力、1 つの span)ならタグは 1 つにします。テレメトリの次元はタグではなくフィールドです。nls の違いは message に反映します。UI が分岐する場合は、フロントエンド/RPC 側でも分けます(UserNotFoundError と ChannelNotFoundError など)。詳しくは references/error-patterns.md を参照してください。
コレクション全体でエラーを蓄積する
最初のエラーで短絡せずに失敗後も処理を続けるには、Either+catchTag+再ループを自作しないでください。Effect.partition(両方のバケット)、Effect.validateAll(全か無か)、または Effect.validateFirst を使用してください。これらは項目ごとの型付きエラーチャネルを回復しますが、中断は捕捉しません。そのため、Cancel が発生するとループ全体が中止されます。
エラーの蓄積と中断に関する注意点、エラーのマッピング、再試行パターンについては references/error-patterns.md を参照してください。
Schema とブランド型のパターン
すべてのエンティティ ID にブランドを付けて、サービス境界を越えた型安全性を確保してください。
このリポジトリでは、Salesforce のレコード ID/組織 ID は UUID ではありません。SalesforceId/OrgId と orgIdFrom/orgIdFromConnection を使用してください(getFields()/Connection、Option、references/schema-patterns.md)。その他の AuthFields には authFieldsFrom/authFieldsFromConnection を使用してください。DefaultOrgInfoSchema.orgId/devHubOrgId は cliId と同様に Schema.optional(OrgId) を使用し、Option は使用しません。
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 TenantId = Schema.UUID.pipe(Schema.brand('@App/TenantId'));
export type TenantId = Schema.Schema.Type<typeof TenantId>;
// Domain types - use Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
tenantId: TenantId,
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)),
tenantId: TenantId
});
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>;
ブランドを付けない場合:
- サービス境界を越えない単純な文字列(URL、ファイルパス)
- プリミティブな設定値
変換や高度なパターンについては references/schema-patterns.md を参照してください。
関数パターン:Effect.gen より Effect.fn を優先
副作用のあるコードでは Effect.fn を優先してください。適切なスパン名による自動トレースが提供されます。スパン名は必須で、local/require-effect-fn-span-name によって強制されます。
共通の .pipe を付けた共有エフェクトが必要な場合に限り、Effect.gen を使用してください。これにより、複数の利用箇所で同じものをそれぞれ .pipe するのを避けられます。たとえば、提供される依存関係、共通のエラーハンドラー、リトライなどです(Runtime ではあまり必要ありません)。サービス定義の本体で使うのは適切です(配線を共有するため)。
// CORRECT - Effect.fn with descriptive name
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
yield* Effect.annotateCurrentSpan('userId', id);
return yield* repo.findById(id);
});
// 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);
// ...
});
// WRONG - params on wrapper arrow, generator has none (closure capture)
// Enforced by local/no-effect-fn-wrapper
const findByIdBad = (id: UserId) =>
Effect.fn('UserService.findById')(function* () {
yield* repo.findById(id); // id from closure
});
// WRONG - Effect.fn invoked immediately (config-enforced effectFnIife). Effect.fn builds a reusable
// function; for one-shot use write Effect.gen and keep the span with a piped withSpan.
const opened = Effect.fn('FsService.open')(function* () {
yield* fs.showTextDocument(uri);
})();
// CORRECT
const openedOk = Effect.gen(function* () {
yield* fs.showTextDocument(uri);
}).pipe(Effect.withSpan('FsService.open'));
// Naming: Don't append Effect. For commands use FooCommand; for helpers/lifecycle use domain names.
// WRONG: logGetEffect, executeAnonymousDocumentEffect, activateEffect
// CORRECT: logGetCommand, executeAnonymousCommand, executeAnonymous (helper), activation (lifecycle)
.pipe、2 回以上読み取る場合に限った const、終端ランナー、ポイントフリーの安全性、Match によるディスパッチ、ガード節、部分列でのリカバリーについては、references/composition-style.md を参照してください。
レイヤーの合成
依存関係は使用箇所ではなく、サービスで宣言してください。
// 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
);
テスト用レイヤーと設定に依存するレイヤーについては、references/layer-patterns.md を参照してください。
Effect が所有するリソース
Effect サービス/レイヤー内で作成されたリソースは、そのスコープに属します。scoped と
Effect.addFinalizer または Effect.acquireRelease を優先してください。dispose を公開したり、ホストのライフサイクルにクリーンアップを委ねたりしないでください。
export class StatusService extends Effect.Service<StatusService>()('StatusService', {
accessors: true,
scoped: Effect.gen(function* () {
const item = vscode.window.createStatusBarItem();
yield* Effect.addFinalizer(() => Effect.sync(() => item.dispose()));
return { show: Effect.sync(() => item.show()) };
})
}) {}
レイヤーの所有者は、そのスコープを閉じる必要があります。ManagedRuntime は自身のレイヤーのスコープを所有するため、拡張機能の
停止時に破棄してください。context.subscriptions は、Effect の所有外で作成されたリソースにのみ使用してください。
Option の処理
Option.getOrThrow は絶対に使用しないでください。必ず両方のケースを明示的に処理してください。
// 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(フロントエンドの状態)
@effect-atom/atom-react を使用したリアクティブな React の状態です。atom はコンポーネントの外で定義し、永続化する必要がある状態には keepAlive を使用します。書き込みには useAtomSet、エフェクトを伴う結果のレンダリングには Result.builder、リスナーのクリーンアップには get.addFinalizer を使用してください。
import { Atom, Result, useAtomValue, useAtomSet } from '@effect-atom/atom-react';
const countAtom = Atom.make(0); // outside the component
const prefsAtom = Atom.make({ theme: 'dark' }).pipe(Atom.keepAlive); // persistent
function Counter() {
const count = useAtomValue(countAtom);
const setCount = useAtomSet(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Effectful atom → Result; handle loading/error/success
function UserProfile() {
return Result.builder(useAtomValue(userAtom))
.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();
}
SubscriptionRef
SubscriptionRef<A> は可変の ref であり、その .changes ストリームは要素 0 として現在の値を必ず出力し、その後すべての将来の変更を出力します。
次のように実装されています(effect/src/internal/subscriptionRef.ts より)。
stream.concat(stream.make(currentValue), stream.fromPubSub(pubsub))
Ref.get と pubsub のサブスクリプションはセマフォ内でアトミックに行われるため、イベントが取りこぼされることはありません。
// WRONG — prepended get is always redundant
Stream.concat(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
Stream.concat(Stream.make(yield* SubscriptionRef.get(ref)), ref.changes)
Stream.merge(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
// CORRECT — .changes already provides the snapshot
ref.changes.pipe(...)
初期スナップショットをスキップするには(たとえば、アクティベーション時の不要な更新を避ける場合)、Stream.drop(1) を使用してください。
グループ化
Object.groupBy の型は Partial<Record<K, V[]>> であるため、すべての利用箇所で undefined をフィルタリングするか、デフォルト値を指定する必要があります。Arr.groupBy は Record<K, NonEmptyArray<V>> を返すため、すべてのキーが存在し、Object.entries にガードは不要です。
import * as Arr from 'effect/Array';
const grouped = Arr.groupBy(ids, id => id.slice(0, 3)); // Record<string, NonEmptyArray<string>>
Object.entries(grouped).map(([prefix, group]) => query(prefix, group));
// vs Object.groupBy, where the same line needs:
// .filter((entry): entry is [string, string[]] => isNotUndefined(entry[1]))
Partial が重要な意味を持つ場合に限り、Object.groupBy を使用してください。たとえば、デフォルト値を指定しながらキーのない場合を分割代入するケースです(const { deploys = [], deleted = [] } = ...)。
アンチパターン(禁止)
上記の「DON'T」列に、禁止されている各パターンが記載されています。references/anti-patterns.md
には、理由と正しい代替手段を含む完全な一覧があります。
空値のガード:宣言された型に合わせる
effect パッケージでは == null / != null は禁止されています(eslint.config.mjs の noNullCompare)。
宣言された型が実際に許容する値に応じて、effect/Predicate のガードを選択してください。ユニオンの形ごとにガードは 1 つで、例外はありません。
| declared type | guard |
|---|---|
T | undefined | isUndefined / isNotUndefined |
T | null | isNull / isNotNull |
T | null | undefined | isNullable / isNotNullable |
import * as Schema from 'effect/Schema';
import { isNotNull, isNotUndefined, isNullable, isUndefined } from 'effect/Predicate';
// T | undefined — the common case (Optional<T>, optional props, ?? sources)
if (isUndefined(maybeValue)) return;
Effect.filterOrFail(isNotUndefined, () => new NotFoundError({ message: '...' }));
// non-id string that must be non-blank — not `isNotUndefined && length > 0`
Effect.filterOrFail(Schema.is(Schema.NonEmptyString), () => new NotFoundError({ message: '...' }));
// AuthFields org id: `orgIdFrom` / `orgIdFromConnection` → `Option<OrgId>`; fail-if-missing via `Option.match`
// DefaultOrgInfo orgId: already `OrgId | undefined` (`Schema.optional(OrgId)` like cliId)
// T | null — e.g. RegExp.exec, JSON payload fields
const match = scriptRegex.exec(html);
if (isNotNull(match)) { … }
// T | null | undefined — e.g. `exclude?: vscode.GlobPattern | null` (optional AND nullable)
const arr = isNullable(exclude) ? undefined : [exclude];
実行時の意味は、isUndefined が x === undefined、isNull が x === null、isNullable がそのいずれかです(node_modules/effect/src/Predicate.ts:611,649,925)。型に必要なものより広いガードでもコンパイルは通りますが、型が生成し得ないケースを含意するため、値について事実と異なる記述に見えます。typeof x === 'object' && x !== null によるオブジェクトの絞り込みはそのままにしてください。これは構造のチェックであり、空値のチェックではありません。
フィルターでのポイントフリーな述語
フィルターの否定には Predicate.not() を使います。ポイントフリーな記述と読みやすさを両立できます。
import { not } from 'effect/Predicate';
// CORRECT — point-free negation
arr.filter(not(isFlowTest))
// AVOID — arrow wrapper around negation
arr.filter(x => !isFlowTest(x))
組み込み(isString、isError、isNotUndefined、isNotNullable)でもカスタムでも、どの述語でも使えます。ラップする述語だけはレシーバーに依存する可能性があります。not(obj.method) はレシーバーを切り離すため、まず実装で this を使っていないことを確認してください(合成スタイルを参照)。
述語が要素レベルでない場合は、既存の名前付き要素述語がある場合、または追加する価値がある場合を除き、! を使います(!isCollectionType(decl.type))。
インポート:@effect/platform からのディープインポートを優先する
@effect/platform からのバレルインポートには HttpApiSwagger(Swagger UI)が含まれますが、esbuild ではツリーシェイキングできません。これにより、Web/デスクトップの各出力バンドルが約5.5MB肥大化し、セキュリティスキャナーで誤検知が発生する場合があります(例:ClamAV シグネチャ)。
サブモジュールは常に名前空間としてインポートしてください(サブモジュール自体が名前空間であり、同名のエクスポートはありません)。
// CORRECT — deep namespace import, tree-shakes unused
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
// use: FetchHttpClient.layer
// WRONG — barrel import drags in HttpApiSwagger
import { FetchHttpClient } from '@effect/platform';
リポジトリの import * as Effect from 'effect/Effect' スタイルに合わせています。すべての @effect/* パッケージに適用されます。コミット前にインポート元を確認してください。
可観測性
// Structured logging
yield * Effect.log('Processing order', { orderId, userId, amount });
// Metrics
const orderCounter = Metric.counter('orders_processed');
yield * Metric.increment(orderCounter);
// Config with validation
const config = Config.all({
port: Config.integer('PORT').pipe(Config.withDefault(3000)),
apiKey: Config.secret('API_KEY'),
maxRetries: Config.integer('MAX_RETRIES').pipe(
Config.validate({ message: 'Must be positive', validation: n => n > 0 })
)
});
メトリクスとトレーシングのパターンについては、references/observability-patterns.md を参照してください。
リファレンスファイル
詳細なパターンについては、references/ ディレクトリ内の以下のリファレンスファイルを参照してください。
composition-style.md-.pipe、2回以上参照する場合に限りconstを使用、末端ランナー、ポイントフリーの安全性、副作用には tap、Match によるディスパッチ、ガード節、ポイントフリーな pipe とジェネレーターのどちらにするか(直線的な本体の場合)、部分列に対するリカバリー(入れ子のEffect.genではなく、最初の Effect から pipe を開始)service-patterns.md- Service の定義、Effect.fn、Context.Tag の例外error-patterns.md- Schema.TaggedError、エラーのマッピング、リトライパターンschema-patterns.md- ブランド型、変換、Schema.Classlayer-patterns.md- 依存関係の合成、テスト用レイヤーanti-patterns.md- 禁止パターンの完全な一覧diagnostics-findings.md- Effect LS の検出項目とその修正方法(ルール別)observability-patterns.md- ロギング、メトリクス、設定のパターン