Syntactic sugar for Effect-TS with for-comprehension style gen blocks.
import { Effect, Race } from "effect"
// Write this:
const fetchUserProfile = (userId: string) => gen {
// Race between cache and API
cached <- Effect.promise(() => cache.get(userId))
[profile, stats] <- Race.race(
fetchFromPrimary(userId),
fetchFromBackup(userId)
)
// Validate and enrich
_ <- validateProfile(profile)
enriched <- enrichWithStats(profile, stats)
// Cache the result
_ <- Effect.promise(() => cache.set(userId, enriched))
return enriched
}
// Instead of this:
const fetchUserProfile = (userId: string) =>
Effect.gen(function* () {
const cached = yield* Effect.promise(() => cache.get(userId))
const [profile, stats] = yield* Race.race(
fetchFromPrimary(userId),
fetchFromBackup(userId)
)
yield* validateProfile(profile)
const enriched = yield* enrichWithStats(profile, stats)
yield* Effect.promise(() => cache.set(userId, enriched))
return enriched
})For projects using standard TypeScript compilation with tsc.
pnpm add -D effect-sugar-tsc ts-patch1. Create tsconfig.json for IDE support:
{
"compilerOptions": {
"plugins": [
{ "name": "effect-sugar-ts-plugin" }
]
}
}2. Create tsconfig.build.json for compilation:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"plugins": [
{
"name": "effect-sugar-tsc",
"transform": "effect-sugar-tsc/transform",
"transformProgram": true
}
]
}
}3. Add build scripts to package.json:
{
"scripts": {
"prepare": "ts-patch install -s",
"build": "tspc --project tsconfig.build.json"
}
}4. Install and build:
pnpm install
pnpm buildWhy separate configs? The compilation transformer operates during TypeScript's program transformation phase, while the IDE plugin works at the language service level. Using separate configs ensures optimal performance and stability in both contexts.
For Vite projects:
pnpm add -D effect-sugar-vite esbuildConfigure in vite.config.ts:
import { defineConfig } from 'vite'
import effectSugar from 'effect-sugar-vite'
export default defineConfig({
plugins: [effectSugar()]
})Add to tsconfig.json for IDE support:
{
"compilerOptions": {
"plugins": [
{ "name": "effect-sugar-ts-plugin" }
]
}
}For the best developer experience, install the VSCode extension:
- Download from the releases page
- Install:
code --install-extension effect-sugar-x.x.x.vsix
The extension provides:
- ✅ Syntax highlighting for gen blocks
- ✅ IntelliSense (hover, go-to-definition, autocomplete)
- ✅ Suppresses TypeScript errors inside gen blocks
For editors other than VSCode (WebStorm, Vim, etc.), add the TypeScript language service plugin:
{
"compilerOptions": {
"plugins": [
{
"name": "effect-sugar-tsc",
"transform": "effect-sugar-tsc/transform",
"transformProgram": true
},
{ "name": "effect-sugar-vscode/ts-plugin" }
]
}
}Then restart your editor's TypeScript server.
Transform gen blocks before linting to prevent syntax errors:
// eslint.config.mjs
import effectSugarPreprocessor from 'effect-sugar-tsc/eslint'
export default [
{
files: ['**/*.ts', '**/*.tsx'],
processor: effectSugarPreprocessor,
// ... your other config
}
]Format gen block code with Prettier using the effect-sugar-format CLI tool:
# Format specific files
npx effect-sugar-format src/**/*.ts
# Format directories
npx effect-sugar-format src/ test/
# Add to package.json scripts
{
"scripts": {
"format": "effect-sugar-format src/"
}
}The CLI tool:
- Transforms
gen {}→Effect.gen()before formatting - Runs Prettier with your project's configuration
- Transforms back to
gen {}syntax
Note: The formatter requires prettier as a peer dependency. Install it with:
pnpm add -D prettierThe recommended setup above works for most projects. For specific build tools or use cases:
- esbuild bundling - For production builds with esbuild, tsup, or unbuild
- tsx runtime with hot reload - For Docker development environments
- Vite -
⚠️ Deprecated, for existing projects only
See the setup guides for more options.
| Input | Output | Notes |
|---|---|---|
x <- effect |
const x = yield* effect |
Bind pattern |
_ <- effect |
yield* effect |
Discard pattern (no binding) |
let x = expr |
const x = expr |
Let binding |
return expr |
return expr |
Return value |
return _ <- effect |
return yield* effect |
Early return (required for type narrowing) |
Fetch data from multiple sources simultaneously and use the first to respond:
import { Effect, fail, raceAll } from "effect"
const fetchProduct = (productId: string) => gen {
metadata <- getProductMetadata(productId)
if (!metadata.isAvailable) {
return _ <- fail(new UnavailableError())
}
data <- raceAll([
gen {
cached <- fetchFromCache(productId)
_ <- validateCache(cached)
enriched <- enrichProductData(cached)
return { source: 'cache', data: enriched }
},
gen {
primary <- fetchFromPrimaryDB(productId)
{ details, inventory } <- getProductDetails(primary.id)
return { source: 'primary', data: { ...primary, details, inventory } }
},
gen {
replica <- fetchFromReplica(productId)
_ <- logReplicaUsage(productId)
return { source: 'replica', data: replica }
}
])
_ <- updateMetrics(data.source)
formatted <- formatProduct(data.data)
return { product: formatted, source: data.source, metadata }
}View the equivalent Effect.gen code
import { Effect } from "effect"
const fetchProduct = (productId: string) =>
Effect.gen(function* () {
const metadata = yield* getProductMetadata(productId)
if (!metadata.isAvailable) {
return yield* Effect.fail(new UnavailableError())
}
const data = yield* Effect.raceAll([
Effect.gen(function* () {
const cached = yield* fetchFromCache(productId)
yield* validateCache(cached)
const enriched = yield* enrichProductData(cached)
return { source: 'cache', data: enriched }
}),
Effect.gen(function* () {
const primary = yield* fetchFromPrimaryDB(productId)
const { details, inventory } = yield* getProductDetails(primary.id)
return { source: 'primary', data: { ...primary, details, inventory } }
}),
Effect.gen(function* () {
const replica = yield* fetchFromReplica(productId)
yield* logReplicaUsage(productId)
return { source: 'replica', data: replica }
})
])
yield* updateMetrics(data.source)
const formatted = yield* formatProduct(data.data)
return { product: formatted, source: data.source, metadata }
})Execute multiple independent operations concurrently with Effect.all:
import { Effect, fail, all } from "effect"
const fetchDashboard = (userId: string) => gen {
user <- getUser(userId)
if (!user.isActive) {
return _ <- fail(new InactiveUserError())
}
[profile, stats, notifications] <- all([
gen {
p <- fetchProfile(user.id)
enriched <- enrichProfile(p)
return enriched
},
gen {
s <- fetchStats(user.id)
_ <- cacheStats(s)
return s
},
gen {
events <- fetchNotifications(user.id)
let unread = events.filter(x => !x.read)
return unread
}
])
return { user, profile, stats, notifications }
}pnpm install
pnpm run build # Build all packages
pnpm test # Run tests
pnpm test:integrationpackages/core/- Core scanner and transformer (effect-sugar-core)packages/tsc-plugin/- ts-patch transformer for tsc (effect-sugar-tsc)packages/esbuild-plugin/- esbuild plugin (effect-sugar-esbuild)packages/vscode-extension/- VSCode extension with bundled TS pluginpackages/vite-plugin/-⚠️ Deprecated - Vite plugin (effect-sugar-vite)
MIT