Learning Effect-TS from the docs in the age of AI
- YT :: https://www.youtube.com/watch?v=O2__t8lceCg
- Original title :: 🚨🚨 LEARNING EFFECTJS - Learning in the age of AI🚨🚨
A two-hour learning stream: Prime reads the Effect v4 (beta) docs line by line, types the examples himself, and refuses to let an agent explain them to him. The trigger is that he already has a large Effect codebase an agent wrote for him from TJ DeVries' spec, and he does not want to live in a codebase he cannot read. TJ joins near the end and pushes him past the part the docs get wrong for newcomers - services and requirements - which is where Effect stops looking like a verbose promise wrapper and starts looking like typed dependency injection.
The method: read docs, don't prompt your way through
The framing question from chat is "how do you motivate yourself to learn a library when AI exists", and the answer is that you still have to know how to use the AI. He deliberately reads rather than asks a model, comparing skipping docs to rushing through math: you miss small concepts, then hit an impenetrable wall built out of everything you skipped. He guesses what each type parameter means before the docs tell him, so he can check his model against theirs.
The sharper version, delivered to a chatter defending vibe-coding: watching things move forward without understanding how they work is the false illusion of progress. You can only advance as far as the thing pushing you forward can advance - past that you are a script kiddie, or a "meat proxy" for the model. The concrete motivation is his own agent-generated client: layers, scoped, provideMainLive, proxy, database live, config, Sentry wiring - "apparently TJ said this is the best way to build, and since I don't know what those words mean, I now have to learn what they mean."
The core of Effect as he reconstructs it
Effect<A, E, R>- success value, error, requirements. An effect is a lazy description of a workflow, not a running computation: unlike a promise constructor, which executes greedily, nothing happens until the runtime interprets it, ideally at one entry point inmain.- Effects are immutable values; every combinator returns a new one.
- The obvious win, which he calls out within five minutes: TypeScript has no notion of
throws(the issue is closed as "not planned"), so error handling is the one thing the type system cannot help you with. Effect makes the error channel mandatory -Effect.fail, tagged errors viaData.TaggedError,catchAllto erase errors,catchTagto peel them off one union member at a time, with the remaining unhandled cases still visible in the type. - Constructors he works through:
succeed,fail,syncandtryfor synchronous thunks (a throw insidesyncbecomes a defect, not an error),promiseandtryPromisefor async,callbackfor callback APIs (single resume, extra calls ignored; an optional secondAbortSignalargument plus a cleanup effect handles interruption), andsuspendto defer effect creation - "an effect that makes an effect". - Structured concurrency:
Effect.allinterrupts the siblings when one branch fails. He contrasts this with the AbortController wiring you write by hand, and agrees with the docs' claim that most production TypeScript backends simply leak every in-flight promise instead.
Rough edges he refuses to pretend are fine: generators (yield vs yield*) which he has never used and which block his understanding of the async parts; the self-referential class-extends-itself idiom for defining services; passing a class name as a string to catchTag; and the docs hiding the underlying error message when JSON.parse fails. He also spends a while on the "Effect Institute" interactive tutorial by Kit Langton - good animations, but it is v3, and he concludes he learns less when his hands are off the keyboard.
Where TJ takes it: services, requirements, layers
TJ's point is that errors are the least interesting third of Effect and the part nobody disputes; the payoff is the R parameter. A service is declared as a type-tagged class exposing effects; yield* on it means "do the effect" and records the dependency in the effect's requirements. If you never provideService, the program does not compile. pipe threads several provideService calls; layers are the way to bundle them.
The arguments that land:
- It replaces the context grab-bag / global object pattern with something the type checker enforces. You cannot run migrations against a Postgres connection you never initialised, because the requirement is visible in the type.
- Testing stops needing mocks. A
UserServicewhose interface isgetUser -> User | NotFoundcan be satisfied in tests by a function that returns literal objects; only the production implementation depends on Postgres. - Requirements do not leak if you construct the dependency inside the block - create the Postgres connection where you create the service, and the outer service's type no longer mentions it.
- The AI angle, and TJ's real reason for liking it: the service boundary is a boundary the agent cannot reach through. You can read the type and know this code does not open a database connection, so the model cannot decide to open and close the pool 85 times inside a loop.
- Schemas over interfaces: TJ forbids the agents in his codebase from writing
interfaceat all - everything is a schema, so data is parsed rather than cast withJSON.parse as MyType. Combined withHttpApi, one schema definition gives you a server, a derived typed fetch client, and enumerated error cases (Prime is visibly pleased to findForbiddenin his own generated API spec). - One correction to the docs: never use bare
Erroras an effect's error type, since it is the parent of all errors and collapses the union - which defeats the entire point.
TJ's closing advice is to stop walking the guide and vibe up the smallest possible HttpApi server or a SQLite to-do CLI, then extend it to a front end, because hovering types in real code teaches more than the tutorial. The stream ends early - Prime's throat injury caps him at about two hours - with a promise to continue by building a server next time.