- Introduction & Core Concepts
- Basic Usage Patterns
- Real-World Examples
- Advanced Features Deep Dive
- Practical Patterns & Best Practices
- Integration Examples
Advanced TypeScript applications require sophisticated type manipulations, utility types, and type-level programming to ensure type safety and developer experience. Without proper utility types, developers often struggle with:
// Traditional approach - manual type manipulation and unsafe operations
interface User {
readonly id: string
readonly name: string
readonly email: string
readonly preferences: {
readonly theme: 'light' | 'dark'
readonly notifications: boolean
}
}
// Manual readonly removal - error-prone and verbose
type MutableUser = {
id: string
name: string
email: string
preferences: {
theme: 'light' | 'dark'
notifications: boolean
}
}
// Manual tuple construction - not type-safe
type ThreeStrings = [string, string, string]
type FourStrings = [string, string, string, string] // Must define each arity manually
// Manual tagged union handling - repetitive and error-prone
type ApiResponse =
| { type: 'success'; data: unknown }
| { type: 'error'; message: string }
| { type: 'loading' }
function handleResponse(response: ApiResponse) {
// Manual type narrowing for each case
if (response.type === 'success') {
return response.data // Type narrowing works but is verbose
}
if (response.type === 'error') {
throw new Error(response.message)
}
// Must handle all cases manually
}
// Complex type equality checks - impossible with standard TypeScript
type IsEqual<A, B> = A extends B ? (B extends A ? true : false) : false
// This approach is flawed and doesn't handle edge cases properlyThis approach leads to:
- Type Manipulation Complexity - Manual utility type creation is verbose and error-prone
- Unsafe Type Operations - No reliable way to manipulate object mutability, extract union members, or perform type-level computations
- Repetitive Boilerplate - Common patterns like tagged unions require manual handling for each type
- Poor Type-Level Abstraction - No composable utilities for advanced type manipulations
Effect's Types module provides battle-tested utility types for advanced TypeScript patterns, type-level programming, and safe type manipulations:
import { Types, Effect, Data } from "effect"
// Type-safe tuple construction
type ExactlyThreeStrings = Types.TupleOf<3, string>
const validTuple: ExactlyThreeStrings = ["a", "b", "c"] // ✅ Valid
// const invalidTuple: ExactlyThreeStrings = ["a", "b"] // ❌ Type error
// Flexible tuple with minimum length
type AtLeastTwoNumbers = Types.TupleOfAtLeast<2, number>
const validMin: AtLeastTwoNumbers = [1, 2, 3, 4, 5] // ✅ Valid
// Safe mutability transformation
interface ReadonlyConfig {
readonly apiUrl: string
readonly timeout: number
readonly retries: number
}
type MutableConfig = Types.Mutable<ReadonlyConfig>
// { apiUrl: string; timeout: number; retries: number }
// Tagged union utilities
type ApiEvent =
| Data.TaggedEnum<{
UserCreated: { userId: string; email: string }
UserUpdated: { userId: string; changes: Record<string, unknown> }
UserDeleted: { userId: string }
}>
function processEvent(event: ApiEvent) {
// Type-safe tag extraction and handling
type EventTags = Types.Tags<ApiEvent> // "UserCreated" | "UserUpdated" | "UserDeleted"
// Safe event extraction by tag
type CreatedEvent = Types.ExtractTag<ApiEvent, "UserCreated">
// { _tag: "UserCreated"; userId: string; email: string }
}
// Advanced type equality with proper edge case handling
type IsUserEqual = Types.Equals<User, User> // true
type IsDifferent = Types.Equals<User, string> // falseTuple Utilities: Type-safe construction of fixed-length and variable-length tuples with exact type constraints
Tagged Union Operations: Extract tags, filter by tag, and manipulate discriminated unions safely
Mutability Control: Transform readonly types to mutable and vice versa, including deep transformations
Type Equality: Reliable type comparison that handles complex cases and edge conditions
Merge Operations: Combine object types with configurable precedence rules
Variance Helpers: Utilities for managing covariance, contravariance, and invariance in generic types
import { Types } from "effect"
// Fixed-length tuples
type Coordinates2D = Types.TupleOf<2, number>
type Coordinates3D = Types.TupleOf<3, number>
const point2D: Coordinates2D = [10, 20] // ✅ Valid
const point3D: Coordinates3D = [10, 20, 30] // ✅ Valid
// Variable-length tuples with minimum requirements
type DatabaseRow = Types.TupleOfAtLeast<3, string | number>
const userRow: DatabaseRow = ["123", "Alice", "alice@test.com", 25, true] // ✅ Valid
// Practical function signature with tuple constraints
function createMatrix<N extends number>(size: N, defaultValue: number): Types.TupleOf<N, Types.TupleOf<N, number>> {
// Implementation would create an N×N matrix
return Array(size).fill(Array(size).fill(defaultValue)) as any
}import { Types, Data } from "effect"
// Define a tagged union for application states
type AppState =
| Data.TaggedEnum<{
Loading: {}
Success: { data: unknown }
Error: { error: string; retryable: boolean }
Idle: {}
}>
// Extract all possible tags
type StateTags = Types.Tags<AppState> // "Loading" | "Success" | "Error" | "Idle"
// Extract specific state by tag
type ErrorState = Types.ExtractTag<AppState, "Error">
// { _tag: "Error"; error: string; retryable: boolean }
// Exclude states by tag
type NonErrorStates = Types.ExcludeTag<AppState, "Error">
// Loading | Success | Idle states only
// Create type-safe state handlers
function handleAppState(state: AppState) {
switch (state._tag) {
case "Loading":
return "Showing spinner..."
case "Success":
return `Data loaded: ${JSON.stringify(state.data)}`
case "Error":
return state.retryable ?
`Error (retryable): ${state.error}` :
`Fatal error: ${state.error}`
case "Idle":
return "Ready to load data"
}
}import { Types } from "effect"
// Configuration objects that need to be mutable for initialization
interface ReadonlyAppConfig {
readonly database: {
readonly host: string
readonly port: number
readonly credentials: {
readonly username: string
readonly password: string
}
}
readonly cache: {
readonly ttl: number
readonly maxSize: number
}
}
// Make all properties mutable for configuration setup
type MutableConfig = Types.Mutable<ReadonlyAppConfig>
// Deep mutability for nested readonly structures
type DeepMutableConfig = Types.DeepMutable<ReadonlyAppConfig>
// Practical configuration initialization
function initializeConfig(): ReadonlyAppConfig {
const config: DeepMutableConfig = {
database: {
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
credentials: {
username: process.env.DB_USER || "user",
password: process.env.DB_PASS || "password"
}
},
cache: {
ttl: 3600,
maxSize: 1000
}
}
// Return as readonly after initialization
return config as ReadonlyAppConfig
}Building a robust API client that handles different response types with complete type safety:
import { Types, Effect, Data } from "effect"
// Define API response types as tagged unions
type ApiResponse<T> = Data.TaggedEnum<{
Success: { data: T; metadata: { requestId: string; timestamp: number } }
ClientError: { status: 400 | 401 | 403 | 404; message: string; details?: Record<string, unknown> }
ServerError: { status: 500 | 502 | 503; message: string; retryAfter?: number }
NetworkError: { cause: "timeout" | "connection" | "dns"; originalError: Error }
}>
// Extract response tags for pattern matching
type ResponseTags<T> = Types.Tags<ApiResponse<T>>
// "Success" | "ClientError" | "ServerError" | "NetworkError"
// Type-safe response handlers using Types utilities
class ApiClient {
private handleResponse<T>(response: ApiResponse<T>) {
return Effect.gen(function* () {
// Use Types.ExtractTag for type-safe extraction
switch (response._tag) {
case "Success": {
const successResponse = response as Types.ExtractTag<ApiResponse<T>, "Success">
yield* Effect.logInfo(`Request ${successResponse.metadata.requestId} succeeded`)
return successResponse.data
}
case "ClientError": {
const clientError = response as Types.ExtractTag<ApiResponse<T>, "ClientError">
return yield* Effect.fail({
type: "ClientError" as const,
status: clientError.status,
message: clientError.message,
retryable: false
})
}
case "ServerError": {
const serverError = response as Types.ExtractTag<ApiResponse<T>, "ServerError">
return yield* Effect.fail({
type: "ServerError" as const,
status: serverError.status,
message: serverError.message,
retryable: true,
retryAfter: serverError.retryAfter
})
}
case "NetworkError": {
const networkError = response as Types.ExtractTag<ApiResponse<T>, "NetworkError">
return yield* Effect.fail({
type: "NetworkError" as const,
cause: networkError.cause,
retryable: networkError.cause !== "dns"
})
}
}
})
}
// Generic request method with full type safety
public request<T>(url: string, options?: RequestInit): Effect.Effect<T, ApiError> {
return Effect.gen(function* () {
const response = yield* Effect.tryPromise({
try: () => fetch(url, options),
catch: (error): ApiResponse<T> => Data.tagged("NetworkError", {
cause: "connection" as const,
originalError: error as Error
})
})
const apiResponse: ApiResponse<T> = yield* Effect.gen(function* () {
if (response.ok) {
const data = yield* Effect.tryPromise(() => response.json())
return Data.tagged("Success", {
data,
metadata: {
requestId: response.headers.get("x-request-id") || "unknown",
timestamp: Date.now()
}
})
}
const errorData = yield* Effect.tryPromise(() => response.json())
if (response.status >= 400 && response.status < 500) {
return Data.tagged("ClientError", {
status: response.status as 400 | 401 | 403 | 404,
message: errorData.message || "Client error",
details: errorData.details
})
}
return Data.tagged("ServerError", {
status: response.status as 500 | 502 | 503,
message: errorData.message || "Server error",
retryAfter: response.headers.get("retry-after") ?
parseInt(response.headers.get("retry-after")!) : undefined
})
}).pipe(
Effect.catchAll(() => Effect.succeed(Data.tagged("NetworkError", {
cause: "timeout" as const,
originalError: new Error("Response parsing failed")
})))
)
return yield* this.handleResponse(apiResponse)
})
}
}
// Usage with complete type safety
interface User {
id: string
name: string
email: string
}
const apiClient = new ApiClient()
const getUserProgram = Effect.gen(function* () {
const user = yield* apiClient.request<User>("/api/users/123")
yield* Effect.logInfo(`Retrieved user: ${user.name}`)
return user
}).pipe(
Effect.catchAll(error => Effect.gen(function* () {
yield* Effect.logError(`API request failed: ${error.message}`)
if (error.retryable) {
yield* Effect.logInfo("Error is retryable, implementing retry logic...")
}
return yield* Effect.fail(error)
}))
)Managing database schema changes while maintaining type safety across application layers:
import { Types, Effect, Data } from "effect"
// Database schema versions using tuple utilities
type SchemaVersions = Types.TupleOfAtLeast<1, number> // At least version 1
// Schema evolution tracking
interface SchemaEvolution<V extends SchemaVersions> {
readonly versions: V
readonly current: V[number]
readonly migrations: Types.TupleOf<V['length'], Migration>
}
// Migration definition using tagged unions
type Migration = Data.TaggedEnum<{
AddColumn: { table: string; column: string; type: string; nullable: boolean }
DropColumn: { table: string; column: string }
CreateTable: { table: string; columns: Record<string, { type: string; nullable: boolean }> }
CreateIndex: { table: string; columns: string[]; unique: boolean }
}>
// User table evolution example
interface UserV1 {
readonly id: string
readonly name: string
readonly email: string
}
interface UserV2 extends UserV1 {
readonly createdAt: Date
readonly updatedAt: Date
}
interface UserV3 extends UserV2 {
readonly profile: {
readonly avatar?: string
readonly bio?: string
}
}
// Type-safe schema evolution manager
class SchemaManager<V extends SchemaVersions> {
constructor(private evolution: SchemaEvolution<V>) {}
// Apply migrations with full type safety
public applyMigrations() {
return Effect.gen(function* () {
const migrations = this.evolution.migrations
yield* Effect.logInfo(`Applying ${migrations.length} migrations`)
// Process each migration with type safety
for (const migration of migrations) {
yield* this.executeMigration(migration)
}
yield* Effect.logInfo(`Schema updated to version ${this.evolution.current}`)
}.bind(this))
}
private executeMigration(migration: Migration) {
return Effect.gen(function* () {
switch (migration._tag) {
case "AddColumn": {
const addCol = migration as Types.ExtractTag<Migration, "AddColumn">
yield* Effect.logInfo(`Adding column ${addCol.column} to ${addCol.table}`)
// Execute SQL: ALTER TABLE ${table} ADD COLUMN ${column} ${type}
break
}
case "DropColumn": {
const dropCol = migration as Types.ExtractTag<Migration, "DropColumn">
yield* Effect.logInfo(`Dropping column ${dropCol.column} from ${dropCol.table}`)
// Execute SQL: ALTER TABLE ${table} DROP COLUMN ${column}
break
}
case "CreateTable": {
const createTable = migration as Types.ExtractTag<Migration, "CreateTable">
yield* Effect.logInfo(`Creating table ${createTable.table}`)
// Execute SQL: CREATE TABLE ${table} (...)
break
}
case "CreateIndex": {
const createIndex = migration as Types.ExtractTag<Migration, "CreateIndex">
const indexType = createIndex.unique ? "UNIQUE INDEX" : "INDEX"
yield* Effect.logInfo(`Creating ${indexType} on ${createIndex.table}`)
// Execute SQL: CREATE [UNIQUE] INDEX ON ${table} (${columns})
break
}
}
})
}
// Type-safe data transformation between schema versions
public transformData<From, To>(
data: From,
fromVersion: number,
toVersion: number
): Effect.Effect<To> {
return Effect.gen(function* () {
let current = data as any
// Apply transformations step by step
for (let version = fromVersion; version < toVersion; version++) {
current = yield* this.transformVersion(current, version, version + 1)
}
return current as To
})
}
private transformVersion(data: unknown, from: number, to: number) {
return Effect.gen(function* () {
yield* Effect.logDebug(`Transforming data from version ${from} to ${to}`)
// Version-specific transformation logic
if (from === 1 && to === 2) {
return {
...data as UserV1,
createdAt: new Date(),
updatedAt: new Date()
} as UserV2
}
if (from === 2 && to === 3) {
return {
...data as UserV2,
profile: {}
} as UserV3
}
return data
})
}
}
// Usage with complete type safety
const userSchemaEvolution: SchemaEvolution<[1, 2, 3]> = {
versions: [1, 2, 3],
current: 3,
migrations: [
Data.tagged("AddColumn", {
table: "users",
column: "created_at",
type: "timestamp",
nullable: false
}),
Data.tagged("AddColumn", {
table: "users",
column: "updated_at",
type: "timestamp",
nullable: false
}),
Data.tagged("AddColumn", {
table: "users",
column: "profile",
type: "jsonb",
nullable: true
})
]
}
const schemaManager = new SchemaManager(userSchemaEvolution)
const migrationProgram = Effect.gen(function* () {
yield* schemaManager.applyMigrations()
// Transform legacy data
const legacyUser: UserV1 = {
id: "123",
name: "Alice",
email: "alice@example.com"
}
const modernUser = yield* schemaManager.transformData<UserV1, UserV3>(
legacyUser,
1,
3
)
yield* Effect.logInfo(`Transformed user: ${JSON.stringify(modernUser)}`)
return modernUser
})Building a configuration system that safely merges multiple configuration sources with type precedence:
import { Types, Effect, Config } from "effect"
// Configuration layers with different priorities
interface DatabaseConfig {
readonly host: string
readonly port: number
readonly database: string
readonly ssl: boolean
}
interface CacheConfig {
readonly redis: {
readonly host: string
readonly port: number
}
readonly ttl: number
readonly maxMemory: string
}
interface AppConfig {
readonly environment: "development" | "staging" | "production"
readonly debug: boolean
readonly database: DatabaseConfig
readonly cache: CacheConfig
}
// Configuration sources with different precedence
type DefaultConfig = Types.DeepMutable<AppConfig>
type EnvironmentConfig = Partial<Types.DeepMutable<AppConfig>>
type FileConfig = Partial<Types.DeepMutable<AppConfig>>
type RuntimeConfig = Partial<Types.DeepMutable<AppConfig>>
// Type-safe configuration merger using Types utilities
class ConfigurationManager {
// Merge configurations with proper precedence (right takes priority)
private mergeConfigs<A, B>(
lower: A,
higher: B
): Types.MergeRight<A, B> {
return this.deepMerge(lower, higher) as Types.MergeRight<A, B>
}
private deepMerge(target: any, source: any): any {
const result = { ...target }
Object.keys(source).forEach(key => {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key])
} else if (source[key] !== undefined) {
result[key] = source[key]
}
})
return result
}
// Load and merge configurations in precedence order
public loadConfiguration(): Effect.Effect<AppConfig> {
return Effect.gen(function* () {
// Default configuration (lowest priority)
const defaultConfig: DefaultConfig = {
environment: "development",
debug: true,
database: {
host: "localhost",
port: 5432,
database: "app_db",
ssl: false
},
cache: {
redis: {
host: "localhost",
port: 6379
},
ttl: 3600,
maxMemory: "100mb"
}
}
// Environment-based configuration
const envConfig: EnvironmentConfig = yield* this.loadEnvironmentConfig()
// File-based configuration
const fileConfig: FileConfig = yield* this.loadFileConfig()
// Runtime configuration (highest priority)
const runtimeConfig: RuntimeConfig = yield* this.loadRuntimeConfig()
// Merge in precedence order using Types utilities
let merged = defaultConfig
merged = this.mergeConfigs(merged, envConfig)
merged = this.mergeConfigs(merged, fileConfig)
merged = this.mergeConfigs(merged, runtimeConfig)
// Validate final configuration
const finalConfig = yield* this.validateConfiguration(merged)
yield* Effect.logInfo("Configuration loaded and validated successfully")
return finalConfig as AppConfig
}.bind(this))
}
private loadEnvironmentConfig(): Effect.Effect<EnvironmentConfig> {
return Effect.gen(function* () {
const env = process.env
const config: EnvironmentConfig = {}
if (env.NODE_ENV) {
config.environment = env.NODE_ENV as "development" | "staging" | "production"
}
if (env.DEBUG) {
config.debug = env.DEBUG === "true"
}
if (env.DB_HOST || env.DB_PORT || env.DB_NAME) {
config.database = {}
if (env.DB_HOST) config.database.host = env.DB_HOST
if (env.DB_PORT) config.database.port = parseInt(env.DB_PORT)
if (env.DB_NAME) config.database.database = env.DB_NAME
if (env.DB_SSL) config.database.ssl = env.DB_SSL === "true"
}
if (env.REDIS_HOST || env.REDIS_PORT) {
config.cache = { redis: {} }
if (env.REDIS_HOST) config.cache.redis.host = env.REDIS_HOST
if (env.REDIS_PORT) config.cache.redis.port = parseInt(env.REDIS_PORT)
}
yield* Effect.logDebug(`Loaded environment config: ${JSON.stringify(config)}`)
return config
})
}
private loadFileConfig(): Effect.Effect<FileConfig> {
return Effect.gen(function* () {
const configPath = process.env.CONFIG_FILE || "./config.json"
const fileContent = yield* Effect.tryPromise({
try: () => import("fs").then(fs => fs.promises.readFile(configPath, "utf-8")),
catch: () => new Error(`Failed to read config file: ${configPath}`)
}).pipe(
Effect.catchAll(() => Effect.succeed("{}")) // Default to empty config if file doesn't exist
)
const config = yield* Effect.try({
try: () => JSON.parse(fileContent) as FileConfig,
catch: () => new Error("Invalid JSON in config file")
})
yield* Effect.logDebug(`Loaded file config: ${JSON.stringify(config)}`)
return config
})
}
private loadRuntimeConfig(): Effect.Effect<RuntimeConfig> {
return Effect.gen(function* () {
// Runtime configuration could come from command line args,
// remote configuration service, etc.
const config: RuntimeConfig = {}
// Example: Override based on command line arguments
if (process.argv.includes("--production")) {
config.environment = "production"
config.debug = false
}
yield* Effect.logDebug(`Loaded runtime config: ${JSON.stringify(config)}`)
return config
})
}
private validateConfiguration(config: any): Effect.Effect<AppConfig> {
return Effect.gen(function* () {
// Type-safe validation using Types utilities
const requiredKeys = ["environment", "debug", "database", "cache"] as const
for (const key of requiredKeys) {
if (!Types.Has<typeof config, typeof key>) {
return yield* Effect.fail(`Missing required configuration: ${key}`)
}
}
// Validate environment values
const validEnvironments = ["development", "staging", "production"] as const
if (!validEnvironments.includes(config.environment)) {
return yield* Effect.fail(`Invalid environment: ${config.environment}`)
}
// Validate database configuration
if (!config.database.host || !config.database.database) {
return yield* Effect.fail("Database configuration incomplete")
}
if (config.database.port < 1 || config.database.port > 65535) {
return yield* Effect.fail("Invalid database port")
}
yield* Effect.logInfo("Configuration validation passed")
return config as AppConfig
})
}
}
// Usage with complete type safety
const configManager = new ConfigurationManager()
const applicationProgram = Effect.gen(function* () {
const config = yield* configManager.loadConfiguration()
yield* Effect.logInfo(`Starting application in ${config.environment} mode`)
yield* Effect.logInfo(`Database: ${config.database.host}:${config.database.port}/${config.database.database}`)
yield* Effect.logInfo(`Cache: ${config.cache.redis.host}:${config.cache.redis.port}`)
return config
}).pipe(
Effect.catchAll(error => Effect.gen(function* () {
yield* Effect.logError(`Configuration loading failed: ${error}`)
return yield* Effect.fail(error)
}))
)The Types module provides sophisticated type equality checking that handles complex edge cases:
import { Types } from "effect"
// Simple type equality
type IsStringString = Types.Equals<string, string> // true
type IsStringNumber = Types.Equals<string, number> // false
// Complex object equality
interface User { id: string; name: string }
interface SameUser { id: string; name: string }
interface DifferentUser { id: string; email: string }
type IsSameUser = Types.Equals<User, SameUser> // true
type IsDifferentUser = Types.Equals<User, DifferentUser> // falseimport { Types } from "effect"
// Custom return types based on equality
type EqualityCheck<A, B> = Types.EqualsWith<A, B, "EQUAL", "NOT_EQUAL">
type StringEquality = EqualityCheck<string, string> // "EQUAL"
type MixedEquality = EqualityCheck<string, number> // "NOT_EQUAL"
// Conditional type definitions based on equality
type ConditionalApi<T, U> = Types.EqualsWith<T, U,
{ type: "same"; api: "unified" },
{ type: "different"; api: "separated" }
>
type SameTypeApi = ConditionalApi<string, string>
// { type: "same"; api: "unified" }
type DifferentTypeApi = ConditionalApi<string, number>
// { type: "different"; api: "separated" }import { Types, Effect } from "effect"
// API version compatibility checker
interface ApiV1 {
readonly version: "1.0"
readonly user: { id: string; name: string }
}
interface ApiV2 {
readonly version: "2.0"
readonly user: { id: string; name: string; email: string }
}
interface ApiV2Compatible {
readonly version: "2.0"
readonly user: { id: string; name: string }
}
// Type-safe compatibility checking
type IsV1V2Compatible = Types.Equals<ApiV1['user'], ApiV2Compatible['user']> // true
type IsV1V2FullyCompatible = Types.Equals<ApiV1, ApiV2> // false
// Build compatibility layer
class ApiCompatibilityLayer {
// Type-safe version handling
public handleRequest<V1, V2>(
v1Request: V1,
v2Request: V2
): Effect.Effect<unknown> {
return Effect.gen(function* () {
type IsCompatible = Types.Equals<V1, V2>
// Use type equality to determine handling strategy
const isCompatible: IsCompatible = Types.Equals<V1, V2>
if (isCompatible) {
yield* Effect.logInfo("Versions are compatible - using unified handler")
// Handle both with same logic
} else {
yield* Effect.logInfo("Versions differ - using compatibility layer")
// Apply transformation logic
}
})
}
}Advanced type manipulation for converting unions to intersections:
import { Types } from "effect"
// Convert union types to intersections
type Union = { a: string } | { b: number } | { c: boolean }
type Intersection = Types.UnionToIntersection<Union>
// { a: string } & { b: number } & { c: boolean }
// Function signature union to intersection
type FunctionUnion =
| ((x: string) => number)
| ((x: number) => boolean)
| ((x: boolean) => string)
type FunctionIntersection = Types.UnionToIntersection<FunctionUnion>
// ((x: string) => number) & ((x: number) => boolean) & ((x: boolean) => string)import { Types, Effect } from "effect"
// Plugin system with union to intersection
interface LoggerPlugin {
log: (message: string) => Effect.Effect<void>
}
interface MetricsPlugin {
increment: (metric: string) => Effect.Effect<void>
gauge: (metric: string, value: number) => Effect.Effect<void>
}
interface AuthPlugin {
authenticate: (token: string) => Effect.Effect<{ userId: string }>
authorize: (userId: string, resource: string) => Effect.Effect<boolean>
}
// Plugin union
type PluginUnion = LoggerPlugin | MetricsPlugin | AuthPlugin
// Convert to intersection for combined functionality
type CombinedPlugins = Types.UnionToIntersection<PluginUnion>
// LoggerPlugin & MetricsPlugin & AuthPlugin
class PluginManager {
private plugins: CombinedPlugins
constructor(plugins: CombinedPlugins) {
this.plugins = plugins
}
// Access all plugin functionality through single interface
public createApplicationServices() {
return Effect.gen(function* () {
// All plugin methods available due to intersection type
yield* this.plugins.log("Application starting")
yield* this.plugins.increment("app.startup")
const auth = yield* this.plugins.authenticate("sample-token")
const authorized = yield* this.plugins.authorize(auth.userId, "dashboard")
if (authorized) {
yield* this.plugins.log(`User ${auth.userId} authorized for dashboard`)
yield* this.plugins.gauge("active.users", 1)
}
}.bind(this))
}
}Deep control over readonly/mutable transformations:
import { Types } from "effect"
// Complex nested readonly structure
interface DeepReadonlyConfig {
readonly app: {
readonly name: string
readonly version: string
readonly features: {
readonly auth: {
readonly enabled: boolean
readonly providers: readonly string[]
}
readonly cache: {
readonly type: "redis" | "memory"
readonly config: {
readonly ttl: number
readonly maxSize: number
}
}
}
}
readonly database: {
readonly connections: readonly {
readonly name: string
readonly config: {
readonly host: string
readonly port: number
}
}[]
}
}
// Transform to fully mutable
type MutableConfig = Types.DeepMutable<DeepReadonlyConfig>
// All readonly modifiers removed recursively
// Selective mutability control
type PartiallyMutable = Types.Mutable<DeepReadonlyConfig>
// Only top-level properties are mutableimport { Types, Effect } from "effect"
// Configuration builder with progressive mutability
class ConfigurationBuilder {
private config: Types.DeepMutable<DeepReadonlyConfig>
constructor() {
this.config = {
app: {
name: "",
version: "1.0.0",
features: {
auth: {
enabled: false,
providers: []
},
cache: {
type: "memory",
config: {
ttl: 3600,
maxSize: 1000
}
}
}
},
database: {
connections: []
}
}
}
// Type-safe configuration methods
public setAppName(name: string): this {
this.config.app.name = name
return this
}
public enableAuth(providers: string[]): this {
this.config.app.features.auth.enabled = true
this.config.app.features.auth.providers = providers
return this
}
public addDatabaseConnection(name: string, host: string, port: number): this {
this.config.database.connections.push({
name,
config: { host, port }
})
return this
}
public setCacheConfig(type: "redis" | "memory", ttl: number, maxSize: number): this {
this.config.app.features.cache.type = type
this.config.app.features.cache.config.ttl = ttl
this.config.app.features.cache.config.maxSize = maxSize
return this
}
// Build final readonly configuration
public build(): Effect.Effect<DeepReadonlyConfig> {
return Effect.gen(function* () {
// Validate configuration
if (!this.config.app.name) {
return yield* Effect.fail("App name is required")
}
if (this.config.database.connections.length === 0) {
return yield* Effect.fail("At least one database connection is required")
}
// Return as readonly
return this.config as DeepReadonlyConfig
}.bind(this))
}
}
// Usage
const configProgram = Effect.gen(function* () {
const config = yield* new ConfigurationBuilder()
.setAppName("MyApp")
.enableAuth(["google", "github"])
.addDatabaseConnection("primary", "localhost", 5432)
.setCacheConfig("redis", 7200, 2000)
.build()
yield* Effect.logInfo(`Built configuration: ${JSON.stringify(config)}`)
return config
})import { Types, Effect, Data } from "effect"
// Factory configuration using utility types
interface FactoryConfig<T> {
readonly name: string
readonly version: string
readonly create: () => Effect.Effect<T>
readonly validate?: (instance: T) => Effect.Effect<boolean>
}
// Factory registry using tagged unions and type utilities
type ServiceFactories = Data.TaggedEnum<{
Database: { config: FactoryConfig<DatabaseService> }
Cache: { config: FactoryConfig<CacheService> }
Logger: { config: FactoryConfig<LoggerService> }
}>
type FactoryTags = Types.Tags<ServiceFactories> // "Database" | "Cache" | "Logger"
// Type-safe factory manager
class ServiceFactory {
private factories = new Map<FactoryTags, FactoryConfig<any>>()
// Register factory with type safety
public register<T, K extends FactoryTags>(
type: K,
config: FactoryConfig<T>
): Effect.Effect<void> {
return Effect.gen(function* () {
yield* Effect.logInfo(`Registering factory: ${type}`)
this.factories.set(type, config)
}.bind(this))
}
// Create service with type safety
public create<K extends FactoryTags>(
type: K
): Effect.Effect<Types.ExtractTag<ServiceFactories, K> extends { config: FactoryConfig<infer T> } ? T : never> {
return Effect.gen(function* () {
const factory = this.factories.get(type)
if (!factory) {
return yield* Effect.fail(`No factory registered for: ${type}`)
}
yield* Effect.logInfo(`Creating service: ${type}`)
const instance = yield* factory.create()
if (factory.validate) {
const isValid = yield* factory.validate(instance)
if (!isValid) {
return yield* Effect.fail(`Validation failed for: ${type}`)
}
}
return instance as any
}.bind(this))
}
}
// Helper for creating type-safe factory configurations
const createFactoryConfig = <T>(config: FactoryConfig<T>): FactoryConfig<T> => config
// Usage with complete type safety
interface DatabaseService {
query: (sql: string) => Effect.Effect<unknown[]>
close: () => Effect.Effect<void>
}
interface CacheService {
get: (key: string) => Effect.Effect<string | null>
set: (key: string, value: string) => Effect.Effect<void>
}
interface LoggerService {
info: (message: string) => Effect.Effect<void>
error: (message: string) => Effect.Effect<void>
}
const serviceFactoryProgram = Effect.gen(function* () {
const factory = new ServiceFactory()
// Register factories
yield* factory.register("Database", createFactoryConfig({
name: "PostgreSQL",
version: "1.0.0",
create: () => Effect.succeed({
query: (sql: string) => Effect.succeed([]),
close: () => Effect.succeed(undefined)
} as DatabaseService),
validate: (db) => Effect.succeed(typeof db.query === 'function')
}))
yield* factory.register("Cache", createFactoryConfig({
name: "Redis",
version: "1.0.0",
create: () => Effect.succeed({
get: (key: string) => Effect.succeed(null),
set: (key: string, value: string) => Effect.succeed(undefined)
} as CacheService)
}))
// Create services with type safety
const database = yield* factory.create("Database") // DatabaseService
const cache = yield* factory.create("Cache") // CacheService
yield* Effect.logInfo("All services created successfully")
return { database, cache }
})import { Types, Effect } from "effect"
// Base configuration interfaces
interface BaseConfig {
readonly name: string
readonly version: string
}
interface DatabaseConfig extends BaseConfig {
readonly host: string
readonly port: number
readonly database: string
}
interface CacheConfig extends BaseConfig {
readonly type: "redis" | "memory"
readonly ttl: number
}
// Environment-specific overrides
interface DevelopmentOverrides {
readonly debug: boolean
readonly logLevel: "debug" | "info"
}
interface ProductionOverrides {
readonly ssl: boolean
readonly monitoring: boolean
}
// Type-safe configuration inheritance
type DevelopmentConfig<T extends BaseConfig> = Types.MergeRight<T, DevelopmentOverrides>
type ProductionConfig<T extends BaseConfig> = Types.MergeRight<T, ProductionOverrides>
// Configuration manager with inheritance
class ConfigManager {
// Create environment-specific configurations
public createDevelopmentConfig<T extends BaseConfig>(
base: T
): Effect.Effect<DevelopmentConfig<T>> {
return Effect.gen(function* () {
const overrides: DevelopmentOverrides = {
debug: true,
logLevel: "debug"
}
const merged = this.mergeConfigs(base, overrides) as DevelopmentConfig<T>
yield* Effect.logInfo("Created development configuration")
return merged
})
}
public createProductionConfig<T extends BaseConfig>(
base: T
): Effect.Effect<ProductionConfig<T>> {
return Effect.gen(function* () {
const overrides: ProductionOverrides = {
ssl: true,
monitoring: true
}
const merged = this.mergeConfigs(base, overrides) as ProductionConfig<T>
yield* Effect.logInfo("Created production configuration")
return merged
})
}
private mergeConfigs<A, B>(base: A, overrides: B): Types.MergeRight<A, B> {
return { ...base, ...overrides } as Types.MergeRight<A, B>
}
}
// Usage with type-safe inheritance
const configProgram = Effect.gen(function* () {
const configManager = new ConfigManager()
const baseDatabaseConfig: DatabaseConfig = {
name: "primary-db",
version: "1.0.0",
host: "localhost",
port: 5432,
database: "app_db"
}
const baseCacheConfig: CacheConfig = {
name: "cache",
version: "1.0.0",
type: "redis",
ttl: 3600
}
// Environment-specific configurations
const devDbConfig = yield* configManager.createDevelopmentConfig(baseDatabaseConfig)
// DatabaseConfig & DevelopmentOverrides
const prodCacheConfig = yield* configManager.createProductionConfig(baseCacheConfig)
// CacheConfig & ProductionOverrides
yield* Effect.logInfo(`Dev DB config has debug: ${devDbConfig.debug}`)
yield* Effect.logInfo(`Prod cache config has monitoring: ${prodCacheConfig.monitoring}`)
return { devDbConfig, prodCacheConfig }
})import { Types, Effect, Data } from "effect"
// Event system with tagged unions and type utilities
type SystemEvents = Data.TaggedEnum<{
UserRegistered: { userId: string; email: string; timestamp: number }
UserLoggedIn: { userId: string; sessionId: string; timestamp: number }
OrderCreated: { orderId: string; userId: string; amount: number; timestamp: number }
PaymentProcessed: { paymentId: string; orderId: string; status: "success" | "failed"; timestamp: number }
}>
type EventTags = Types.Tags<SystemEvents>
// "UserRegistered" | "UserLoggedIn" | "OrderCreated" | "PaymentProcessed"
// Event handler types
type EventHandler<T extends EventTags> = (
event: Types.ExtractTag<SystemEvents, T>
) => Effect.Effect<void>
// Type-safe event bus
class EventBus {
private handlers = new Map<EventTags, EventHandler<any>[]>()
// Register type-safe event handlers
public on<T extends EventTags>(
eventType: T,
handler: EventHandler<T>
): Effect.Effect<void> {
return Effect.sync(() => {
const existingHandlers = this.handlers.get(eventType) || []
this.handlers.set(eventType, [...existingHandlers, handler])
})
}
// Emit events with type safety
public emit<T extends EventTags>(
event: Types.ExtractTag<SystemEvents, T>
): Effect.Effect<void> {
return Effect.gen(function* () {
const handlers = this.handlers.get(event._tag) || []
yield* Effect.logInfo(`Emitting event: ${event._tag}`)
// Execute all handlers for this event type
yield* Effect.all(
handlers.map(handler => handler(event)),
{ concurrency: "unbounded" }
)
}.bind(this))
}
// Subscribe to multiple event types
public onMultiple<T extends readonly EventTags[]>(
eventTypes: T,
handler: (event: Types.ExtractTag<SystemEvents, T[number]>) => Effect.Effect<void>
): Effect.Effect<void> {
return Effect.all(
eventTypes.map(eventType =>
this.on(eventType, handler as EventHandler<any>)
)
).pipe(Effect.map(() => {}))
}
}
// Event handler implementations
const createEventHandlers = (eventBus: EventBus) => {
return Effect.gen(function* () {
// User-related event handlers
yield* eventBus.on("UserRegistered", event => Effect.gen(function* () {
yield* Effect.logInfo(`New user registered: ${event.email}`)
// Send welcome email, create user profile, etc.
}))
yield* eventBus.on("UserLoggedIn", event => Effect.gen(function* () {
yield* Effect.logInfo(`User logged in: ${event.userId}`)
// Update last login time, track analytics, etc.
}))
// Order-related event handlers
yield* eventBus.on("OrderCreated", event => Effect.gen(function* () {
yield* Effect.logInfo(`Order created: ${event.orderId} for $${event.amount}`)
// Send order confirmation, update inventory, etc.
}))
yield* eventBus.on("PaymentProcessed", event => Effect.gen(function* () {
if (event.status === "success") {
yield* Effect.logInfo(`Payment successful: ${event.paymentId}`)
// Send receipt, fulfill order, etc.
} else {
yield* Effect.logError(`Payment failed: ${event.paymentId}`)
// Send payment failure notification, retry logic, etc.
}
}))
// Multi-event handler for analytics
yield* eventBus.onMultiple(
["UserRegistered", "UserLoggedIn", "OrderCreated"] as const,
event => Effect.gen(function* () {
yield* Effect.logInfo(`Analytics: Processing ${event._tag} event`)
// Send to analytics service
})
)
})
}
// Usage with complete type safety
const eventSystemProgram = Effect.gen(function* () {
const eventBus = new EventBus()
// Set up event handlers
yield* createEventHandlers(eventBus)
// Emit events with type safety
yield* eventBus.emit(Data.tagged("UserRegistered", {
userId: "user123",
email: "alice@example.com",
timestamp: Date.now()
}))
yield* eventBus.emit(Data.tagged("OrderCreated", {
orderId: "order456",
userId: "user123",
amount: 99.99,
timestamp: Date.now()
}))
yield* eventBus.emit(Data.tagged("PaymentProcessed", {
paymentId: "pay789",
orderId: "order456",
status: "success",
timestamp: Date.now()
}))
yield* Effect.logInfo("All events processed successfully")
})Combining Types utilities with Schema for advanced validation patterns:
import { Types, Effect, Schema, Data } from "effect"
// Schema-driven type generation with Types utilities
const BaseUserSchema = Schema.Struct({
id: Schema.String,
name: Schema.String,
email: Schema.String
})
const UserPreferencesSchema = Schema.Struct({
theme: Schema.Literal("light", "dark"),
notifications: Schema.Boolean,
language: Schema.String
})
// Combine schemas using Types utilities
type BaseUser = Schema.Schema.Type<typeof BaseUserSchema>
type UserPreferences = Schema.Schema.Type<typeof UserPreferencesSchema>
type UserWithPreferences = Types.MergeRight<BaseUser, { preferences: UserPreferences }>
// Dynamic schema composition
class SchemaComposer {
// Merge schemas with type safety
public static composeUserSchema<T extends Schema.Schema.All>(
baseSchema: typeof BaseUserSchema,
extensionSchema: T
): Schema.Schema<Types.MergeRight<BaseUser, Schema.Schema.Type<T>>> {
return Schema.Struct({
...baseSchema.fields,
...extensionSchema.fields
}) as any
}
// Create schema variants using Types utilities
public static createMutableSchema<T extends Schema.Schema.All>(
schema: T
): Schema.Schema<Types.Mutable<Schema.Schema.Type<T>>> {
// Transform readonly schema to mutable version
return schema as any
}
}
// Usage with Effect integration
const schemaValidationProgram = Effect.gen(function* () {
// Create extended user schema
const ExtendedUserSchema = SchemaComposer.composeUserSchema(
BaseUserSchema,
Schema.Struct({ preferences: UserPreferencesSchema })
)
// Validate user data
const userData = {
id: "123",
name: "Alice",
email: "alice@example.com",
preferences: {
theme: "dark" as const,
notifications: true,
language: "en"
}
}
const validatedUser = yield* Schema.decodeUnknown(ExtendedUserSchema)(userData)
yield* Effect.logInfo(`Validated user: ${JSON.stringify(validatedUser)}`)
return validatedUser
})Advanced error handling patterns using Types utilities:
import { Types, Effect, Data } from "effect"
// Error taxonomy using tagged unions
type ApplicationErrors = Data.TaggedEnum<{
ValidationError: { field: string; message: string; value: unknown }
DatabaseError: { operation: string; cause: string; retryable: boolean }
NetworkError: { url: string; status?: number; timeout: boolean }
AuthenticationError: { reason: "invalid_token" | "expired" | "missing" }
AuthorizationError: { resource: string; action: string; userId: string }
}>
type ErrorTags = Types.Tags<ApplicationErrors>
// Error recovery strategies
class ErrorRecoveryManager {
// Type-safe error classification
public classifyError(error: ApplicationErrors): Effect.Effect<{
severity: "low" | "medium" | "high"
retryable: boolean
userMessage: string
}> {
return Effect.gen(function* () {
switch (error._tag) {
case "ValidationError": {
const validationError = error as Types.ExtractTag<ApplicationErrors, "ValidationError">
return {
severity: "low" as const,
retryable: false,
userMessage: `Invalid ${validationError.field}: ${validationError.message}`
}
}
case "DatabaseError": {
const dbError = error as Types.ExtractTag<ApplicationErrors, "DatabaseError">
return {
severity: "high" as const,
retryable: dbError.retryable,
userMessage: "A database error occurred. Please try again."
}
}
case "NetworkError": {
const networkError = error as Types.ExtractTag<ApplicationErrors, "NetworkError">
return {
severity: networkError.timeout ? "medium" : "high",
retryable: true,
userMessage: "Network connection failed. Please check your connection."
}
}
case "AuthenticationError": {
const authError = error as Types.ExtractTag<ApplicationErrors, "AuthenticationError">
return {
severity: "medium" as const,
retryable: authError.reason === "expired",
userMessage: "Authentication failed. Please log in again."
}
}
case "AuthorizationError": {
return {
severity: "medium" as const,
retryable: false,
userMessage: "You don't have permission to perform this action."
}
}
}
})
}
// Recovery strategy based on error type
public createRecoveryStrategy<T>(
operation: Effect.Effect<T, ApplicationErrors>
): Effect.Effect<T, ApplicationErrors> {
return operation.pipe(
Effect.catchAll(error => Effect.gen(function* () {
const classification = yield* this.classifyError(error)
yield* Effect.logError(`Error occurred: ${error._tag}`)
yield* Effect.logInfo(`Severity: ${classification.severity}, Retryable: ${classification.retryable}`)
if (classification.retryable) {
yield* Effect.logInfo("Attempting recovery...")
// Implement retry logic based on error type
return yield* this.attemptRecovery(operation, error)
}
// Non-retryable error - re-throw with context
return yield* Effect.fail(error)
}))
)
}
private attemptRecovery<T>(
operation: Effect.Effect<T, ApplicationErrors>,
originalError: ApplicationErrors
): Effect.Effect<T, ApplicationErrors> {
return Effect.gen(function* () {
// Implement error-specific recovery logic
switch (originalError._tag) {
case "DatabaseError": {
yield* Effect.sleep("1 second")
return yield* operation
}
case "NetworkError": {
yield* Effect.sleep("2 seconds")
return yield* operation
}
default:
return yield* Effect.fail(originalError)
}
})
}
}
// Usage with comprehensive error handling
const errorHandlingProgram = Effect.gen(function* () {
const recoveryManager = new ErrorRecoveryManager()
// Simulate operations that might fail
const riskyDatabaseOperation = Effect.gen(function* () {
// Simulate database error
return yield* Effect.fail(Data.tagged("DatabaseError", {
operation: "select",
cause: "Connection timeout",
retryable: true
}))
})
const riskyNetworkOperation = Effect.gen(function* () {
// Simulate network error
return yield* Effect.fail(Data.tagged("NetworkError", {
url: "https://api.example.com/users",
status: 503,
timeout: false
}))
})
// Apply recovery strategies
const databaseResult = yield* recoveryManager.createRecoveryStrategy(riskyDatabaseOperation)
const networkResult = yield* recoveryManager.createRecoveryStrategy(riskyNetworkOperation)
return { databaseResult, networkResult }
}).pipe(
Effect.catchAll(error => Effect.gen(function* () {
yield* Effect.logError(`Unrecoverable error: ${error._tag}`)
return null
}))
)Comprehensive testing patterns for Types-based code:
import { Types, Effect, TestClock, TestContext } from "effect"
// Type-safe test utilities
class TypesTestUtils {
// Compile-time type equality assertions
public static assertTypeEquals<A, B>(): Types.Equals<A, B> extends true ? void : never {
// This will cause a compile error if types are not equal
return undefined as any
}
// Runtime type compatibility checking
public static checkTypeCompatibility<A, B>(
valueA: A,
valueB: B
): Effect.Effect<boolean> {
return Effect.sync(() => {
// Use JSON serialization for basic structural comparison
try {
const serializedA = JSON.stringify(valueA)
const serializedB = JSON.stringify(valueB)
return typeof valueA === typeof valueB
} catch {
return false
}
})
}
// Property-based testing for type transformations
public static testMutabilityTransformation<T>(
readonlyValue: T
): Effect.Effect<boolean> {
return Effect.sync(() => {
type MutableVersion = Types.Mutable<T>
// Verify that mutable version has same keys but without readonly
const original = readonlyValue as any
const keys = Object.keys(original)
// Test that transformation maintains structure
return keys.length > 0 // Basic structural check
})
}
}
// Test suite for Types utilities
const typesTestSuite = Effect.gen(function* () {
yield* Effect.logInfo("Running Types utility tests...")
// Compile-time type equality tests
TypesTestUtils.assertTypeEquals<string, string>() // ✅ Should compile
// TypesTestUtils.assertTypeEquals<string, number>() // ❌ Would cause compile error
// Test tuple type generation
const testTuple: Types.TupleOf<3, string> = ["a", "b", "c"]
yield* Effect.logInfo(`Tuple test passed: ${JSON.stringify(testTuple)}`)
// Test mutability transformation
interface TestReadonly {
readonly prop: string
readonly nested: {
readonly value: number
}
}
const readonlyObj: TestReadonly = {
prop: "test",
nested: { value: 42 }
}
const mutabilityTest = yield* TypesTestUtils.testMutabilityTransformation(readonlyObj)
yield* Effect.logInfo(`Mutability transformation test: ${mutabilityTest ? "PASS" : "FAIL"}`)
// Test tagged union operations
type TestUnion = { _tag: "A"; valueA: string } | { _tag: "B"; valueB: number }
type ExtractedA = Types.ExtractTag<TestUnion, "A">
type UnionTags = Types.Tags<TestUnion>
// Runtime verification
const taggedValue: TestUnion = { _tag: "A", valueA: "test" }
const hasCorrectTag = taggedValue._tag === "A"
yield* Effect.logInfo(`Tagged union test: ${hasCorrectTag ? "PASS" : "FAIL"}`)
yield* Effect.logInfo("All Types utility tests completed")
return "Tests completed successfully"
})
// Integration with Effect's testing framework
const testProgram = typesTestSuite.pipe(
Effect.provide(TestContext.TestContext),
Effect.catchAll(error => Effect.gen(function* () {
yield* Effect.logError(`Test failed: ${JSON.stringify(error)}`)
return "Tests failed"
}))
)Types provides essential utility types for advanced TypeScript patterns, type-level programming, and safe type manipulations in Effect applications.
Key benefits:
- Type Safety: Compile-time guarantees for complex type operations and transformations
- Composability: Utility types that work together to build sophisticated type systems
- Developer Experience: Reduced boilerplate and improved IDE support for advanced TypeScript patterns
The Types module is particularly valuable when building type-safe APIs, configuration systems, plugin architectures, and any application requiring advanced type manipulations. It bridges the gap between TypeScript's type system and practical application development, providing battle-tested utilities for common type-level programming tasks.