-
b56ade1: Rename the hook-local Result adapters:
queryOptions→resultQueryOptionsandmutationOptions→resultMutationOptions.Breaking change. Update imports and call sites:
-import { queryOptions, mutationOptions } from "wellcrafted/query"; +import { resultQueryOptions, resultMutationOptions } from "wellcrafted/query";
createQueryFactories,defineQuery, anddefineMutationare unchanged — they still compose through the renamed adapters, so there is still exactly one place that unwrapsResultinto TanStack's throwing contract.Why: the old names collided with TanStack Query's own
queryOptions/mutationOptionsidentity helpers. Theresult*prefix removes that collision and names what the adapters do — adapt aResult-returningqueryFn/mutationFn— so both can coexist in one file without aliasing.This clarifies the two-family model:
resultQueryOptions/resultMutationOptions— hook-local, reactive, or local-QueryClientoperations passed straight to a framework hook.createQueryFactories(...).defineQuery/defineMutation— reusable,QueryClient-bound handles with imperative helpers (.fetch,.ensure, callable mutations).
Cache operations (
invalidateQueries,setQueryData,ensureQueryData, …) stay on theQueryClientand are intentionally not wrapped.
- f7f627b: Add
onceat the newwellcrafted/functionsubpath.once(fn)runs the wrapped function at most once: the first call invokes it and caches the result, and every later call returns that cached result while ignoring any arguments passed after the first. It is a dependency-free combinator for idempotent disposal and lazy one-time initialization.
-
4575de2: Simplify the imperative factory API so query reads choose a cache policy explicitly and mutations use the callable form as their only imperative trigger.
Queries now expose:
.fetch(): run through TanStack Query's freshness-awarefetchQuerypath..ensure(): return cached data if it exists, fetching only when the cache has no data.
This makes the read policy visible at the call site:
const freshEnough = await userQuery.fetch(); const cacheFirst = await userQuery.ensure();
Mutations no longer expose a duplicate
.execute()helper:const saved = await saveUser(input);
The richer TanStack mutation observer surface still lives behind
.optionsfor hooks.
-
fc1c87e: Add
queryOptionsandmutationOptionsas the canonical Result-to-TanStack adapters.wellcrafted/querynow exposes two lower-level helpers alongsidedefineQuery/defineMutation:queryOptions(input)accepts aqueryKeyplus a Result-returningqueryFnand returns standardQueryObserverOptionswhosequeryFnresolvesOk(data)and throwsErr(error).mutationOptions(input)accepts amutationKeyplus a Result-returningmutationFnand returns standardMutationOptionswith the same Result unwrapping behavior.
These helpers are platform-agnostic: no
QueryClientrequired. They compose cleanly inside framework hooks:import { queryOptions, mutationOptions } from "wellcrafted/query"; const user = createQuery(() => queryOptions({ queryKey: ["user", userId], queryFn: () => services.getUser(userId), }) ); const save = createMutation(() => mutationOptions({ mutationKey: ["saveUser"], mutationFn: (input: SaveUserInput) => services.saveUser(input), }) );
defineQueryanddefineMutationnow compose through these helpers, so there is exactly one canonical conversion path fromResult<TData, TError>to TanStack's throwing contract. The.optionsproduced bydefineQueryanddefineMutationis the same shape returned byqueryOptionsandmutationOptions.Use
queryOptions/mutationOptionswhen options are local to a hook call site. UsedefineQuery/defineMutationwhen you also want imperative helpers (.fetch,.ensure,.execute, callable form) bound to a specificQueryClient.The new helpers share names with TanStack Query's framework-adapter identity helpers (
@tanstack/react-query,@tanstack/svelte-query). This is intentional: Wellcrafted's versions are the Result-aware equivalents. If you need both in one file, alias one on import.
-
92a38df: Add
defineKeysand infer literalmutationKeytuples indefineMutationwithoutas const.defineMutationnow uses TypeScript 5.0'sconsttype parameter modifier onTMutationKey, mirroring the recent change todefineQuery.mutationKey: ['users', 'create']infers asreadonly ['users', 'create']instead of widening tostring[].// Before defineMutation({ mutationKey: ['users', 'create'] as const, mutationFn: ... }); // After defineMutation({ mutationKey: ['users', 'create'], mutationFn: ... });
defineKeysis a new identity helper for declaring TanStack Query key maps. It uses theconstmodifier to preserve tuple types on static entries, and a strict tuple constraint to narrow factory return shapes withoutas const. Factory bodies still needas constfor literal narrowing of static positions.import { defineKeys } from "wellcrafted/query"; const userKeys = defineKeys({ all: ["users"], // readonly ['users'] active: ["users", "active"], // readonly ['users', 'active'] detail: (id: string) => ["users", id], // [string, string] page: (n: number) => ["users", n] as const, // readonly ['users', number] });
Without
defineKeys, the same map requiresas conston every line. The const modifier on a generic does not reach into function-body return-type inference, which is why factory entries with literal positions like'users'still needas constinside the body to preserve the literal.
-
b75fffd: Infer literal
queryKeytuples indefineQuerywithout requiringas const. TheTQueryKeytype parameter now uses TypeScript 5.0'sconstmodifier, soqueryKey: ['users', userId]is inferred asreadonly ['users', string]instead of being widened tostring[]. This preserves the narrow key type on.options.queryKeyand insidequeryFn's context, matching how TanStack Query types keys when you write them inline.Before:
const userQuery = defineQuery({ queryKey: ["users", userId] as const, // `as const` required to keep the tuple queryFn: ({ queryKey }) => services.getUser(queryKey[1]), });
After:
const userQuery = defineQuery({ queryKey: ["users", userId], // inferred as readonly ['users', string] queryFn: ({ queryKey }) => services.getUser(queryKey[1]), });
-
2b47280: Remove
defineHttpErrorsand its type companions.Bumped as
minor(pre-1.0 convention: minor is breaking in 0.x) rather thanmajor.wellcrafted@1.0.0was published and unpublished on 2025-07-17 and npm permanently blocks reusing that version number, so a major bump from 0.37.0 -> 1.0.0 cannot publish.defineHttpErrorscoupled error definition to one specific transport (HTTP). The right primitive isdefineErrors, with HTTP status (or any other per-variant metadata) carried in a sibling map keyed by variant name. This deletion shrinks the public surface and keepswellcrafted/errortransport-agnostic, so the same errors can flow through HTTP, queues, RPC, CLIs, or anywhere else without per-transport variants of the primitive.Removed exports:
defineHttpErrorsHttpErrorFactoryHttpErrorsConfigValidatedHttpConfigDefineHttpErrorsReturnInferHttpErrorInferHttpErrors
Replace the tuple-configured factory with
defineErrorsplus asatisfies-checked side-map:// before import { defineHttpErrors, type InferHttpErrors } from "wellcrafted/error"; const AiChatError = defineHttpErrors({ Unauthorized: [401, () => ({ message: "Unauthorized" })], ProviderNotConfigured: [ 503, ({ provider }: { provider: string }) => ({ message: `${provider} not configured`, provider, }), ], }); type AiChatError = InferHttpErrors<typeof AiChatError>; return c.json( AiChatError.ProviderNotConfigured({ provider }), AiChatError.ProviderNotConfigured.status );
// after import { defineErrors, type InferErrors } from "wellcrafted/error"; const AiChatError = defineErrors({ Unauthorized: () => ({ message: "Unauthorized" }), ProviderNotConfigured: ({ provider }: { provider: string }) => ({ message: `${provider} not configured`, provider, }), }); type AiChatError = InferErrors<typeof AiChatError>; const AiChatErrorStatus = { Unauthorized: 401, ProviderNotConfigured: 503, } as const satisfies Record<AiChatError["name"], number>; return c.json( AiChatError.ProviderNotConfigured({ provider }), AiChatErrorStatus.ProviderNotConfigured );
The
satisfies Record<E["name"], number>clause enforces exhaustiveness at compile time: adding a variant without a status (or a stale key after a rename) is a type error.The same shape generalizes to any other per-variant metadata you want to attach without polluting the error body or forking the primitive: icons, log levels, retry classifications, gRPC codes, problem-type URIs, etc. Each lives in its own
as const satisfies Record<E["name"], T>table next to the consumer that cares.
-
30f0711: Add
defineHttpErrors: typed HTTP error factories with a static.statusproperty.Like
defineErrors, but each variant is paired with its HTTP status code via a[status, factory]tuple. The status is attached to the factory function as a literal type (e.g.AssetError.MissingFile.statusis400), never serialized into the error body.const AssetError = defineHttpErrors({ MissingFile: [400, () => ({ message: "Missing file" })], FileTooLarge: [ 413, ({ size }: { size: number }) => ({ message: `File too large: ${size}`, size, }), ], StorageLimitExceeded: [402, () => ({ message: "Storage limit exceeded" })], }); // Hono route handler: return c.json(AssetError.MissingFile(), AssetError.MissingFile.status); // body: { error: { name: 'MissingFile', message: 'Missing file' }, data: null } // status: 400
Also exports
InferHttpError,InferHttpErrors,HttpErrorFactory,DefineHttpErrorsReturn,HttpErrorsConfig, andValidatedHttpConfig.
-
b9b0dce: Add
parseJsonandJsonParseErrortowellcrafted/json.parseJson(text)parses a JSON string into aResult<JsonValue, JsonParseError>instead of throwing. It is the Result-returning counterpart toJSON.parse:- the success value is typed as
JsonValuerather thanany, so you must narrow or validate it before treating it as a known shape - malformed input is reported as a tagged
JsonParseError(built withdefineErrors) carrying the underlyingSyntaxErrorascause, instead of throwing
import { parseJson } from "wellcrafted/json"; const { data, error } = parseJson(raw); if (error) return Err(error); // JsonParseError data; // JsonValue
No reviver argument is accepted: a reviver can return arbitrary values, which would make the
JsonValuesuccess type unsound. - the success value is typed as
-
5c4dab6: Add the
wellcrafted/testingentry point withexpectOkandexpectErr.These are test-only assertion helpers for
Resultvalues. Each unwraps the expected branch and returns it narrowed, throwing if theResultis the other variant:import { expectOk, expectErr } from "wellcrafted/testing"; const value = expectOk(parseConfig(raw)); // success value, narrowed const error = expectErr(parseConfig("x")); // error value, narrowed expect(error.name).toBe("ConfigParseError");
They intentionally throw: a failed expectation should abort the test, which every runner reports as a failure. Throwing is fenced into this separate
wellcrafted/testingentry point sowellcrafted/resultstays throw-free. The helpers throw a plainError, so they work under bun, vitest, jest, ornode:testwithout depending on a test framework.
-
ea6597e: Add
wellcrafted/logger— a structured, level-keyed logger for libraries that usedefineErrors.- Five levels (
trace/debug/info/warn/error), nofatal. Mirrors Rust'stracing. log.warn(err)/log.error(err)take a typed error unary (fromdefineErrors), accepting both the raw variant and theErr<>wrapper — level is a call-site decision, never a property of the variant.log.info/log.debug/log.traceare free-form (message + optional data).- DI-only: no global registry, no default logger singleton. Every consumer takes a
log?: Loggeroption. - Ships pure-JS sinks (
consoleSink,memorySink,composeSinks) and thetapErrResult-flow combinator (mirrors Rust's.inspect_err).tapErris re-exported from bothwellcrafted/logger(canonical use) andwellcrafted/result(where it structurally belongs).
Runtime-agnostic. Environment-specific sinks (e.g. a Bun JSONL file sink) belong in downstream packages.
The
LoggableError = AnyTaggedError | Err<AnyTaggedError>union is safe to discriminate via"name" in err:nameis already stamped (and reserved) bydefineErrorson every tagged error, andErr<E>has no top-levelname. No new field reservation was needed.import { createLogger, consoleSink, tapErr } from "wellcrafted/logger"; const log = createLogger("my-source"); // defaults to consoleSink const result = await tryAsync({ try: () => writeTable(path), catch: (cause) => MyError.WriteFailed({ path, cause }), }).then(tapErr(log.warn));
- Five levels (
-
687485e: Fix
trySyncandtryAsynctype inference for catch handlers returning union Err typesWhen a catch handler returned multiple Err variants (e.g.,
Err<A> | Err<B>), TypeScript could not infer the union, requiring explicit return type annotations. Replaced three overloads per function with a single generic signature that infers the full catch return type.
-
7e1da94: Move
JsonValueandJsonObjecttypes towellcrafted/jsonexport path.BREAKING:
JsonValueandJsonObjectare no longer exported fromwellcrafted/error. Import them fromwellcrafted/jsoninstead.
-
1c6e597: BREAKING:
defineErrorsv2 — Rust-style namespaced errors with Err-by-default- Factories now return
Err<...>directly (no dualFooError/FooErrfactories) - Keys are short variant names (
Connection,Parse) instead ofConnectionError InferError<T>takes a single factory (typeof HttpError.Connection)InferErrors<T>replacesInferErrorUnion<T>for union extractionValidatedConfigprovides descriptive error when reservednamekey is used
- Factories now return
- 4539eb2: Redesign
createTaggedErrorbuilder: flat.withFields()API replaces nested.withContext()/.withCause(),.withMessage()is optional and seals the message (not in factory input type),messagerequired at call site when.withMessage()is absent. Removescontextnesting,causeas first-class field, andreasonconvention.
-
69bf59e: Breaking:
createTaggedErrornow requires.withMessage(fn)as a mandatory terminal builder step before factories are available..withMessage(fn)is now required: Factories (XxxError,XxxErr) are only returned after calling.withMessage(). Previously you could destructure factories after just.withContext()or.withCause().- Message is sealed by template: When
.withMessage()is used, themessagefield is not in the factory input type — it is derived entirely from the template function. Without.withMessage(),messageis required at the call site. - Context type tightened:
contextconstraint changed fromRecord<string, unknown>toJsonObject(values must be JSON-serializable). - Strict builder/factory separation:
ErrorBuilderonly has chain methods (.withContext(),.withCause(),.withMessage()).FinalFactoriesonly has factory functions — no more mixing.
Before:
const { UserError, UserErr } = createTaggedError("UserError").withContext<{ userId: string; }>(); // ❌ Error: factories not available without .withMessage()
After:
const { UserError, UserErr } = createTaggedError("UserError") .withContext<{ userId: string }>() .withMessage(({ context }) => `User ${context.userId} failed`); // ✅ Factories now available
-
f2b3ebd: Add Standard Schema v1 compliant Result wrappers for schema validation libraries
New
wellcrafted/standard-schemamodule provides:OkSchema(schema)- wraps any Standard Schema into{ data: T, error: null }ErrSchema(schema)- wraps any Standard Schema into{ data: null, error: E }ResultSchema(dataSchema, errorSchema)- creates discriminated union of Ok | Err
Works with Zod, Valibot, ArkType, and any Standard Schema v1 compliant library. Preserves schema capabilities (validate, jsonSchema) based on input schema features.
-
7df7aee: Fix Brand type to support hierarchical/stacked brands
Previously, stacking brands resulted in
neverbecause intersecting{ [brand]: 'A' }with{ [brand]: 'B' }collapsed the property tonever.Now brands use a nested object structure
{ [brand]: { [K in T]: true } }(following Effect-TS pattern), allowing brand stacking to work correctly:type AbsolutePath = string & Brand<"AbsolutePath">; type ProjectDir = AbsolutePath & Brand<"ProjectDir">; const projectDir = "/home/project" as ProjectDir; const abs: AbsolutePath = projectDir; // Works - child assignable to parent
-
1237ce4: BREAKING: Rename
resultQueryFntoqueryFnandresultMutationFntomutationFnThe
resultprefix was redundant since the TypeScript signature already encodes that these functions return Result types. This removes unnecessary Hungarian notation from the API.Migration:
// Before defineQuery({ queryKey: ["users"], resultQueryFn: () => getUsers(), }); defineMutation({ mutationKey: ["users", "create"], resultMutationFn: (input) => createUser(input), }); // After defineQuery({ queryKey: ["users"], queryFn: () => getUsers(), }); defineMutation({ mutationKey: ["users", "create"], mutationFn: (input) => createUser(input), });
-
7c41baf: feat(error): explicit opt-in for context and cause properties
Following Rust's thiserror pattern,
createTaggedErrornow uses explicit opt-in forcontextandcauseproperties. By default, errors only have{ name, message }.Breaking Change: Previously, all errors had optional
contextandcauseproperties by default. Now you must explicitly chain.withContext<T>()and/or.withCause<T>()to add these properties.Before (old behavior):
const { ApiError } = createTaggedError("ApiError"); // ApiError had: { name, message, context?: Record<string, unknown>, cause?: AnyTaggedError }
After (new behavior):
// Minimal error - only name and message const { ApiError } = createTaggedError("ApiError"); // ApiError has: { name, message } // With required context const { ApiError } = createTaggedError("ApiError").withContext<{ endpoint: string; }>(); // ApiError has: { name, message, context: { endpoint: string } } // With optional typed cause const { ApiError } = createTaggedError("ApiError").withCause< NetworkError | undefined >(); // ApiError has: { name, message, cause?: NetworkError }
Migration: To replicate the old permissive behavior, either specify the types explicitly:
const { FlexibleError } = createTaggedError("FlexibleError") .withContext<Record<string, unknown> | undefined>() .withCause<AnyTaggedError | undefined>();
Or use the new defaults by calling without generics:
const { FlexibleError } = createTaggedError("FlexibleError") .withContext() // Defaults to Record<string, unknown> | undefined .withCause(); // Defaults to AnyTaggedError | undefined
-
b917060: Replace
defineErrorwith fluentcreateTaggedErrorAPIThe
createTaggedErrorfunction now uses a fluent builder pattern for type constraints:// Simple usage (flexible mode) const { NetworkError, NetworkErr } = createTaggedError("NetworkError"); // Required context const { ApiError } = createTaggedError("ApiError").withContext<{ endpoint: string; status: number; }>(); // Optional typed context const { LogError } = createTaggedError("LogError").withContext< { file: string; line: number } | undefined >(); // Chaining both context and cause const { RepoError } = createTaggedError("RepoError") .withContext<{ entity: string }>() .withCause<DbError | undefined>();
Breaking changes:
defineErrorhas been removed (usecreateTaggedErrorinstead)- The old
createTaggedErrorgeneric overloads are removed in favor of the fluent API
-
5f0e7af: Make query and mutation definitions directly callable
Query and mutation definitions from
defineQueryanddefineMutationare now directly callable functions:// Queries - callable defaults to ensure() behavior const { data, error } = await userQuery(); // same as userQuery.ensure() // Mutations - callable defaults to execute() behavior const { data, error } = await createUser({ name: "John" }); // same as createUser.execute()
The explicit methods (
.ensure(),.fetch(),.execute()) remain available for when you need different behavior or prefer explicit code.Breaking change:
.optionsis now a property instead of a function. UpdatecreateQuery(query.options())tocreateQuery(query.options).
-
5d72453: feat(error): support optional typed context via union with undefined
You can now specify context that is optional but still type-checked when provided by using a union with
undefined:type LogContext = { file: string; line: number } | undefined; const { LogError } = createTaggedError<"LogError", LogContext>("LogError"); // Context is optional LogError({ message: "Parse failed" }); // But when provided, it's typed LogError({ message: "Parse failed", context: { file: "app.ts", line: 42 } });
This gives you the best of both worlds: optional context like flexible mode, but with type enforcement like fixed context mode.
The same pattern works for
cause:type NetworkError = TaggedError<"NetworkError">; type CauseType = NetworkError | undefined; const { ApiError } = createTaggedError< "ApiError", { endpoint: string }, CauseType >("ApiError");
-
5d72453: fix(error): simplify TaggedError types for better ReturnType inference
Previously,
createTaggedErrorused function overloads to provide precise call-site type inference. While this worked well for constructing errors, it brokeReturnType<typeof MyError>because TypeScript picks the last overload (the most constrained signature).This change simplifies to single signatures per mode:
- Flexible mode (no type params): context and cause are optional with loose typing
- Fixed context mode (TContext specified): context is required with exact type
- Both fixed mode (TContext + TCause): context required, cause optional but constrained
ReturnTypenow works correctly:const { NetworkError } = createTaggedError("NetworkError"); type NetworkError = ReturnType<typeof NetworkError>; // = TaggedError<'NetworkError'> with optional context/cause
BREAKING CHANGE: In flexible mode,
contextis now typed asRecord<string, unknown> | undefinedinstead of precisely inferred at call sites. Users who need typed context should use fixed context mode:// Before (flexible with inference - no longer works) const { ApiError } = createTaggedError("ApiError"); const err = ApiError({ message: "x", context: { endpoint: "/users" } }); // After (fixed context mode) const { ApiError } = createTaggedError<"ApiError", { endpoint: string }>( "ApiError" ); const err = ApiError({ message: "x", context: { endpoint: "/users" } });
-
2388e95: fix(result): add overload for trySync/tryAsync when catch returns Ok | Err union
Previously, trySync and tryAsync only had overloads for catch handlers that returned exclusively Ok or exclusively Err. This caused type errors when a catch handler could return either based on runtime conditions (conditional recovery pattern).
Added a third overload to both functions that accepts catch handlers returning
Ok<T> | Err<E>, properly typing the return asResult<T, E>.
-
0143960: Align
TaggedErrortype parameter order withcreateTaggedErrorChanged
TaggedError<TName, TCause, TContext>toTaggedError<TName, TContext, TCause>so both APIs use consistent ordering. This simplifies context-only error definitions:Before:
type NetworkError = TaggedError<"NetworkError", never, { host: string }>;
After:
type NetworkError = TaggedError<"NetworkError", { host: string }>;
Single-generic usage (
TaggedError<"Name">) is unaffected.
-
e724405: Add function overloads to
createTaggedErrorfor opt-in type strictnessThe
createTaggedErrorfunction now supports three overload signatures:- Fully flexible (default): No generics required, both context and cause are optional
- Context fixed: Specify
TContextgeneric to require context, cause remains flexible - Both fixed: Specify both
TContextandTCausegenerics to require both
This allows callers to opt-in to stricter type safety when needed while maintaining backward compatibility with the flexible default behavior. When a type is explicitly specified at factory creation time, it becomes required at every call site.
Updated type definitions:
FlexibleTaggedErrorFactories,ContextFixedTaggedErrorFactories,BothFixedTaggedErrorFactoriesFlexibleTaggedErrorConstructorFn,ContextFixedTaggedErrorConstructorFn,BothFixedTaggedErrorConstructorFn- And corresponding
Errconstructor function variants
-
60e3439: feat(error): add typed context and cause support to createTaggedError
Introduces three usage modes for
createTaggedError:- Flexible mode: Context and cause are optional with any shape
- Fixed context mode: Context is required with an exact type
- Both fixed mode: Context required, cause constrained to specific type
The
TaggedErrortype now uses conditional types so thatcontextandcauseproperties only exist when specified, making the types more precise.// Mode 1: Flexible const { NetworkError } = createTaggedError("NetworkError"); NetworkError({ message: "Timeout" }); NetworkError({ message: "Timeout", context: { url: "..." } }); // Mode 2: Fixed context (context required) type BlobContext = { filename: string; code: "INVALID" | "TOO_LARGE" }; const { BlobError } = createTaggedError<"BlobError", BlobContext>( "BlobError" ); BlobError({ message: "Invalid", context: { filename: "x", code: "INVALID" }, }); // Mode 3: Both fixed const { ApiError } = createTaggedError<"ApiError", ApiContext, NetworkError>( "ApiError" );
-
a95c8f8: Improve type inference for tagged error factories
Refactored
createTaggedErrorto automatically infer cause and context types from input, eliminating the need for explicit generic parameters. AddedTContextgeneric parameter toTaggedErrortype for better type safety. Reorganized internal type structure with separateTaggedErrorConstructorFnandTaggedErrConstructorFntypes for improved modularity.
- 0e5f291: Improve extractErrorMessage with comprehensive primitive handling and extended error property checking
-
ae5792f: feat: simplify error system with unified TaggedError type
- Combined
BaseErrorandTaggedErrorinto a single unified type - Made
causeproperty optional and strongly typed asTaggedErrorfor error chaining - Created JSON-serializable call stacks through error chaining
- Each error in the chain maintains full TypeScript typing and context
- Updated documentation with comprehensive examples of error chaining patterns
BREAKING CHANGE:
BaseErrortype has been removed. UseTaggedErrororTaggedError<string>instead. - Combined
-
1c0d30f: feat: add typed cause generic to TaggedError for type-safe error chaining
- Add second generic parameter
TCausetoTaggedErrortype - Enable compile-time validation of error cause relationships
- Use
TaggedError<string, any>constraint to avoid circular references - Add comprehensive JSDoc with JSON serialization examples
- Update all documentation to show typed cause
- Maintain full backward compatibility with existing single-generic usage
Example:
type NetworkError = TaggedError<"NetworkError">; type DatabaseError = TaggedError<"DatabaseError", NetworkError>; const dbError: DatabaseError = { name: "DatabaseError", message: "Connection failed", cause: networkError, // TypeScript enforces NetworkError type };
- Add second generic parameter
- cb062a9: chore: upgrade Biome to v2.2.6 and migrate configuration
-
8b752cd: Rename mapErr to catch and add smart return type narrowing to trySync/tryAsync
Breaking Change: The
mapErrparameter intrySyncandtryAsyncfunctions has been renamed tocatchfor better semantic clarity.New Feature: Added function overloads that provide smart return type narrowing:
- When
catchalways returnsOk<T>, the function returnsOk<T>(guaranteed success) - When
catchcan returnErr<E>, the function returnsResult<T, E>(may succeed or fail)
This eliminates unnecessary error checking when you know your error handler always recovers, while still requiring proper error handling when failures are possible.
Migration: Replace
mapErrwithcatchin alltrySyncandtryAsynccalls:// Before trySync({ try: () => operation(), mapErr: (error) => Err(new Error("Failed")), }); // After trySync({ try: () => operation(), catch: (error) => Err(new Error("Failed")), });
Improved JSDoc documentation with clearer examples and better hover experience.
- When
- e07df18: Fixes QueryClient cache-access functions to return TQueryData instead of TData
- 2a966f8: Fixes isResult() to allow { data: null, error: null } as valid Ok(null) result
- 851983e: Updates types to include
TDatagenerics for improved type accuracy
- 48048a2: Adds default generics for
defineMutation. Because we set a default genericvoidforTVariables, we can callmutation.mutate()instead of having to always put inmutation.mutate({}).
-
5c73e8e: Renames
mapErrorparameter tomapErrintrySyncandtryAsyncfunctions.The
mapErrparameter now returnsErr<E>directly instead of just the error valueE. This change provides more explicit control over error wrapping.Migration required:
// Before trySync({ try: () => operation(), mapError: (error) => ({ name: "MyError", message: "Operation failed", cause: error, }), }); // After trySync({ try: () => operation(), mapErr: (error) => Err({ name: "MyError", message: "Operation failed", cause: error, }), });
-
af03f84: Rename fetchCached method to fetch in query definitions
BREAKING CHANGE: The
fetchCached()method on query definitions has been renamed tofetch()to better reflect its actual behavior. This method intelligently fetches data from cache OR network based on staleness.Migration:
- Replace all
.fetchCached()calls with.fetch() - No functional changes - behavior remains identical
- See MIGRATION_FETCHCACHED_TO_FETCH.md for detailed migration guide
Before:
const { data, error } = await userQuery.fetchCached();
After:
const { data, error } = await userQuery.fetch();
- Replace all
- a4efb1b: Replaces QueryOptions with QueryObserverOptions
-
83e90fe: Add TanStack Query integration utilities
- New
createQueryFactoriesfunction for creating type-safe query and mutation definitions - Automatic Result type handling for TanStack Query
- Dual interface pattern: reactive (for UI) and imperative (for actions)
- Full TypeScript support with proper type inference
- Comprehensive documentation and examples
- New
-
5257a07: Add createTaggedError factory function for generating tagged error constructors
Introduces a new utility function that creates factory functions for tagged errors:
createTaggedError(name)returns two factory functions:- One that creates plain TaggedError objects (named with "Error" suffix)
- One that creates Err-wrapped TaggedError objects (named with "Err" suffix)
- Automatic naming convention: "Error" suffix becomes "Err" suffix for Result-wrapped version
- Fully typed with TypeScript generics for type safety
- Comprehensive JSDoc documentation with examples
This simplifies creating tagged error constructors and provides a consistent API for both plain errors and Result-wrapped errors.
Example usage:
const { NetworkError, NetworkErr } = createTaggedError("NetworkError"); // Creates plain error object const error = NetworkError({ message: "Connection failed", context: { url }, cause: undefined, }); // Creates Err-wrapped error for Result types return NetworkErr({ message: "Connection failed", context: { url }, cause: undefined, });
-
deb7bff: Make context property optional in TaggedError type
The context property in BaseError and TaggedError is now optional, allowing developers to omit it when no contextual information is needed. This reduces boilerplate code and eliminates the need for empty context objects.
Before:
return Err({ name: "ValidationError", message: "Invalid input", context: {}, // Required but meaningless cause: null, });
After:
return Err({ name: "ValidationError", message: "Invalid input", cause: null, // context can be omitted });
This is a backwards-compatible change - all existing code continues to work unchanged.
- 062ac62: Adds dts to tsdown config, fixes missing types
- b450bd1: Adds
Brandtype for nominal typing in Typescript - 662cd35: Rebrands repo to "wellcrafted" with modular architecture
- 95f7704: Renames
unwrapIfResultfunction toresolve
- c5a8d2a: Adds
unwrapfunction to extract success value fromResult<T, E>and throw error forErr<E>variant
- 27c88a4: Fixes
partitionResultsfunction to have default empty oks and errs arrays
- 0416841: Renames
mapErrtomapError, enhance docs, introduce naming conventions for error suffix - a708959: Renames
mapErrtomapError, enhances result and error handling documentation, and introduces naming conventions for error types
- efbe590: Adds
BaseError,TaggedError,extractErrorMessage, and comprehensive error handling guide
- 3d0539e: Add
declarationMap: trueto tsconfig
- 05a04b8: Adds unwrapIfResult function
- 422f86c: Adds isResult type guard
- d5f209a: Migrates to tsdown
- 0937db9: Adds files field to package.json
- f5ac7c1: Adds main, module, and types for improved module resolution to package.json
- 0f9bf08: Updates Typescript module and lib to NodeNext and ESNext
- 208a06d: Adds output support for CommonJS
- c750a55: Adds partitionResults utility function to separate Result objects into success and error arrays
- 55cfbff: Enhances Result documentation and add extraction utilities
- d6d6b6e: Updates TypeScript configuration and package.json exports
- ff97dbb: Adds partitionResults utility function to separate Result objects into success and error arrays
- b85bd17: Adds "require" to exports for more compatibility
- 5cfccdd: Fixes tsup config and entry points
- 0b32ddd: Updates mapErr handling in trySync and tryAsync functions, now automatically wrapping with Err<>
- 50498bf: Removes discriminated union implementation of result in favor of exclusive (data/error) pattern
- 1c4ef64: Make second arg of mutate optional, use Partial Typescript helper
- 5e6133a: Added new onSuccessLocal, onErrorLocal, onSettledLocal
- d1d5257: Restore old ok and err behavior
- fe51ba1: Updated generics of trySync and tryAsync again
- 344f3d5: Update generics in trySync and tryAsync functions; remove services.ts
- 7f49d16: Add OnMutateError generic to createMutation function
- 57a88a6: Simplify Result type definition by consolidating Ok and Err types
- 35dbcdd: Update mapError to take in Err and spit out Err
- 75df94c: Rename catch parameter to mapErr in trySync and tryAsync functions for improved clarity
- 9b4bf3e: createMutation function returns function directly
- 0cf54d2: Update first parameter of createMutation for improved clarity
- 043d624: Add InferOk and InferErr utility types for Result type inference
- b4a0bc4: Add ServiceFn type and update exports in index.ts
- 9598e2a: Add createMutation function
- 53e8cf2: Make MutationFn callbacks optional
- 2bbf651: Make callbacks optional
- 69be638: Fix callback type definitions in MutationFn for improved clarity and consistency
- 5739f07: Correct type definition for tryAsync function to ensure it always returns a Promise
- 732009a: Rename and update ServiceResultFactoryFns to ServiceErrorFns for improved clarity and consistency in error handling
- 6c1b688: Update type definition for tryAsync function to enforce promise return type
- c39ff0f: Export service-related functions from index
- fe27e9b: Update ServiceResultFactoryFns type definitions for improved clarity and consistency
- 7b3a558: Create service result factory functions
- 5aec8f9: Simplify logic of createServiceResultFactoryFns
- 42b2163: Extracted /index to /result and reexported
- dd4ce91: Simplify Ok and Err functions
- 4485244: Initial features scaffolded (ok, err, result, trySync, tryAsync)
- 6ef1580: Added keywords to package metadata to improve npm discoverability