Core Concepts
Every program depends on capabilities it does not build itself — a clock, a database, somewhere to send email. Effect models each of these as a Service, and Hyperlinks build directly on that model. This page starts with Services and adds one idea at a time.
Services and Tags
A Service is a capability your program depends on. Rather than thread it through function after function, you refer to it through a Tag: a typed name that stands for the Service everywhere it is used. Your code declares what it needs, and the type system keeps track of it for you.
Working with a Service is three steps — define it, use it, and provide it:
import { import ContextContext, import EffectEffect } from "effect"
// define: a service and its interface, named by a tag
class class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
}
Random extends import ContextContext.const Service: <Random, {
readonly next: Effect.Effect<number>;
}>() => <Identifier, E, R, Args>(id: Identifier, options?: {
readonly make: ((...args: Args) => Effect.Effect<{
readonly next: Effect.Effect<number>;
}, E, R>) | Effect.Effect<{
readonly next: Effect.Effect<number>;
}, E, R> | undefined;
} | undefined) => Context.ServiceClass<Random, Identifier, {
readonly next: Effect.Effect<number>;
}> & ([unassigned] extends [R] ? unknown : {
...;
}) (+2 overloads)
Creates a Context service key.
When to use
Use when you need to define a context service key for a dependency that must
be provided by the surrounding context.
Details
Call Context.Service("Key") for a function-style key, or use the two-stage
form Context.Service<Self, Shape>()("Key") for class-style service
declarations. The returned key can be yielded as an Effect and passed to
Context.make, Context.add, and the Context getter functions.
Gotchas
The string key is the runtime identity of the service. Reusing the same key
string for unrelated services makes them occupy the same slot in a
Context.
Example (Creating service keys)
import { Context } from "effect"
// Create a simple service
const Database = Context.Service<{
query: (sql: string) => string
}>("Database")
// Create a service class
class Config extends Context.Service<Config, {
port: number
}>()("Config") {}
// Use the services to create contexts
const db = Context.make(Database, {
query: (sql) => `Result: ${sql}`
})
const config = Context.make(Config, { port: 8080 })
Service<class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
}
Random, {
readonly next: Effect.Effect<number, never, never>(property) next: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
next: import EffectEffect.interface Effect<out A, out E = never, out R = never>The Effect interface defines a value that lazily describes a workflow or
job. The workflow requires some context R, and may fail with an error of
type E, or succeed with a value of type A.
When to use
Use when you need to represent a lazy, composable workflow that can require
services, fail with a typed error, or succeed with a typed value.
Details
Effect values model resourceful interaction with the outside world,
including synchronous, asynchronous, concurrent, and parallel interaction.
They use a fiber-based concurrency model, with built-in support for
scheduling, fine-grained interruption, structured concurrency, and high
scalability.
To run an Effect value, you need a Runtime, which is a type that is
capable of executing Effect values.
Effect<number>
}>()("app/Random") {}You reach the Service by yielding its Tag, and you supply an Implementation once, at the edge of the program, with a Layer. The Tag sits between the two: the single point where a capability is asked for on one side and fulfilled on the other. Because that point is explicit, you can provide the real Service in production, a stub in a test, or swap one for another — without touching the code that depends on it.
From Services to Contracts
Hyperlink starts where Effects Services leave off. A Hyperlink is a Service, but its Tag declares a Contract: the Hyperlinks methods, together with a schema for every value that passes through them.
import * as import HyperlinkHyperlink from "hyperlink-ts/Hyperlink"
import { import SchemaSchema } from "effect"
class class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect<void, never, never>;
};
}
Counter extends import HyperlinkHyperlink.Tag<class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect<void, never, never>;
};
}
Counter>()("app/Counter", {
value: anyvalue: import HyperlinkHyperlink.ref(import SchemaSchema.const Number: Schema.Numberconst Number: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: Schema.MakeOptions) => number;
makeOption: (input: number, options?: Schema.MakeOptions) => Option<number>;
makeEffect: (input: number, options?: Schema.MakeOptions) => Effect<number, Schema.SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
Type-level representation of
Number
.
Schema for number values, including NaN, Infinity, and -Infinity.
Details
Default JSON serializer:
- Finite numbers are serialized as numbers.
- Non-finite values are serialized as strings (
"NaN", "Infinity", "-Infinity").
Number), // observable state
increment: any(property) increment: {
kind: MethodKind;
payload: P;
success: Su;
error: E;
stream: Str;
annotations: Ann;
annotate: <A extends MethodAnnotations>(annotations: A) => Method<P, Su, E, Str, Ann & A, Client>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
increment: import HyperlinkHyperlink.effectFn({ by: Schema.Number(property) by: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: Schema.MakeOptions) => number;
makeOption: (input: number, options?: Schema.MakeOptions) => Option<number>;
makeEffect: (input: number, options?: Schema.MakeOptions) => Effect<number, Schema.SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
by: import SchemaSchema.const Number: Schema.Numberconst Number: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: Schema.MakeOptions) => number;
makeOption: (input: number, options?: Schema.MakeOptions) => Option<number>;
makeEffect: (input: number, options?: Schema.MakeOptions) => Effect<number, Schema.SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
Type-level representation of
Number
.
Schema for number values, including NaN, Infinity, and -Infinity.
Details
Default JSON serializer:
- Finite numbers are serialized as numbers.
- Non-finite values are serialized as strings (
"NaN", "Infinity", "-Infinity").
Number }), // a call, with a typed argument
}) {}That difference is what makes a Hyperlink cross-runtime. An ordinary Service is an interface for one runtime to satisfy. A Contract, because every value it names is a schema, is an interface that can be satisfied across runtimes — the schemas are enough to carry each call over the wire. The seam a Tag creates, once a line between modules, can now be a line between processes.
The same Tag, wherever it runs
You declare a Hyperlink once. Where it runs, you decide later — with the Layer you provide:
const const inProcess: anyconst inProcess: {
build: (memoMap: MemoMap, scope: Scope) => Effect.Effect<Context<Counter | Hyperlink.Local<Counter>>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
inProcess = import HyperlinkHyperlink.layer(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
groupId: string;
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, const counterImpl: Effect.Effect<
{
value: any
increment: ({
by,
}: {
readonly by: number
}) => Effect.Effect<void, never, never>
},
never,
never
>
const counterImpl: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
counterImpl) // run it in this runtime
const const served: anyconst served: {
build: (memoMap: MemoMap, scope: Scope) => Effect.Effect<Context<Counter | Hyperlink.Local<Counter> | Handler<'value'> | Handler<'increment'>>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
served = import HyperlinkHyperlink.serve(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
groupId: string;
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, const counterImpl: Effect.Effect<
{
value: any
increment: ({
by,
}: {
readonly by: number
}) => Effect.Effect<void, never, never>
},
never,
never
>
const counterImpl: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
counterImpl) // expose it over HTTP
const const client: anyconst client: {
build: (memoMap: MemoMap, scope: Scope) => Effect.Effect<Context<Counter>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
client = import HyperlinkHyperlink.connect(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
groupId: string;
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, import HyperlinkHyperlink.protocolHttp(4000)) // reach one running elsewhere (server / CLI)
// A browser dashboard opens many live streams — serve with Node.wsServer and connect with
// Hyperlink.ws (WebSocket), or an HTTP client starves at the browser's connection cap.
// See Managing Layers for the full set of provide/serve/client layers.
Whichever you choose, yield* Counter returns the same Handle. Reading a value, calling a method, watching it change — the code reads identically whether the Hyperlink sits beside it or across a network. Only the Layer changes. That is what cross-runtime means.
The shape of a Contract
A Contracts methods take a small number of forms:
Hyperlink.effect(schema)— a value to read.Hyperlink.effectFn(input, output?)— a call that takes an argument.Hyperlink.ref(schema)— observable state: read it with.get, follow it through.changes.Hyperlink.stream(schema)— a continuous stream of values.
Nodes
When a program spans more than one runtime, each runtime is a Node. A Node carries the address at which its Hyperlinks can be reached, and served Hyperlinks find one another through the Nodes they share. You reach for Nodes only when a Hyperlink is served or distributed; a single-runtime program needs none. Fleets & Peers covers them in full.
In brief
A Tag names a Hyperlink. Its Contract describes the methods and their schemas. An Implementation fulfils the Contract, and a Layer places it — in process, served, or reached as a client. The Handle you get from the Tag is the same in every case.
Next
Put it together in Creating a Hyperlink.