Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime

Migrating to _tag and providedIn#

Two renames on the public API:

ts
// exceptions
craftException({ code: 'UserNotFound' }, payload)
craftException({ _tag: 'UserNotFound' }, payload)   // after

// services
craftService({ name: 'UserApi', scope: 'global' }, …)
craftService({ name: 'UserApi', providedIn: 'global' }, …)   // after

A codemod ships with this release and does most of the work:

bash
node node_modules/@craft-ts/dev-tools/craft-migrate-errors/rename-field.mjs \
  tsconfig.json --from=scope --to=providedIn --not-with=_tag

It is compiler-driven and AST-based: it only rewrites where tsc actually breaks, and only in positions the AST confirms are the field. Run it per tsconfig, then re-run until it reports clean.

Read this before you trust a green build#

The dangerous part of this migration is invisible.

If your code reads the discriminant in a value position, the compiler will point at every site and you cannot get it wrong:

ts
craftException({ code: 'X' })   // errors: '_tag' is missing
exception.code                  // errors: no such property

If your code reads it in a type position — a conditional or an Extract — it does not error. It resolves to never, and everything downstream quietly becomes empty:

ts
type CodesOf<E> = E extends { code: infer C } ? C : never;   // -> never
type Only<E>    = Extract<E, { readonly code: string }>;      // -> never
type HasScope<V> = V extends { scope: unknown } ? … : …;      // -> the else branch

Nothing fails. No test goes red. The capability just stops existing.

This happened four times inside CraftTS itself while performing this migration, each caught late and by accident:

What brokeHow it surfaced
route exhaustiveness (CraftExceptionCodes)one dev-tools test that compiles fixtures expected to FAIL
component exception codesa runtime template test, several commits later
settled exception codesa type assertion in an unrelated spec
route HTTP dependency derivationa stale-looking assertion, chased on a hunch

In every case the whole library suite — 1489 tests — was green.

What to do about it#

Run the finder before you run the codemod, and again afterwards:

bash
node node_modules/@craft-ts/dev-tools/craft-migrate-errors/find-silent-sites.mjs code src
node node_modules/@craft-ts/dev-tools/craft-migrate-errors/find-silent-sites.mjs scope src

It walks the AST for the two shapes that cannot fail loudly — a conditional whose extends clause reads the field, and an Extract/Exclude/Omit/Pick over a literal containing it — and exits non-zero while any remain. Everything it lists must be migrated by hand: the codemod cannot see them, because the compiler never reports them.

Ten such positions existed inside CraftTS. Four were found by accident, over several days. The other six took this tool about a second.

And keep at least one test that asserts something must not compile (@ts-expect-error, or a fixture your build is supposed to reject). It is the only kind of test that notices a guarantee disappearing.

What did NOT change#

scope on an exception is untouched:

ts
craftException({ _tag: 'UserNotFound', scope: 'loader' }, payload)

It says where an exception came from — an origin, not a container or a lifetime — so it keeps its name. Only the service scope became providedIn.

Also unchanged: the HTTP client's { source: 'code' } matcher and the code field of a server response body. Those are the server's vocabulary, not CraftTS's, and the codemod leaves them alone by construction.

Performance#

None of this costs anything. Measured across the rename:

  • discrimination: 6.8 ns/op on both sides;

  • test-suite wall time: +0.02%;

  • type-check instantiations on the published build: −0.00%.

@craft-ts/effect is marginally cheaper afterwards, because mapping an Effect error onto a craft exception stopped being a transposition and became the identity — Effect and CraftTS now discriminate on the same field.

On this page

  • Read this before you trust a green build
  • What to do about it
  • What did NOT change
  • Performance

@craft-ts · MIT licensed

GitHubnpm

Search