@padosoft/utilities
v1.5.0
Published
Zero-dependency TypeScript utilities: typed query helpers, Configuration store, Zod utilities, and general-purpose type helpers
Readme
@padosoft/utilities
Zero-dependency TypeScript utilities: typed React Query helpers, a reactive Configuration store, Zod utilities, and general-purpose type helpers.
Installation
npm install @padosoft/utilities
# zod is a required peer dependency
npm install zodQuery helpers
Framework-agnostic utilities for React Query (and any compatible hook library).
defineQuery
Pairs a key factory with a fn factory so call-site params are specified exactly once:
import { defineQuery } from "@padosoft/utilities/lib/query";
const userQuery = defineQuery({
queryKey: ({ id }) => ["users", id],
queryFn: ({ id }) => api.getUser(id),
});
// In a component:
useQuery({ ...userQuery.query({ id: "123" }), staleTime: 60_000 });
// For cache invalidation:
queryClient.invalidateQueries({ queryKey: userQuery.key({ id: "123" }) });defineQueryGroup
Groups related queries under a shared base key; group.base() invalidates all of them at once:
import { defineQueryGroup, defineQuery } from "@padosoft/utilities/lib/query";
const usersQueries = defineQueryGroup({
baseKey: ["users"],
queries: {
single: defineQuery({ queryKey: ({ id }) => [id], queryFn: ({ id }) => api.getUser(id) }),
list: defineQuery({ queryKey: (p) => ["list", p], queryFn: (p) => api.listUsers(p) }),
},
});
usersQueries.base(); // → ["users"]
usersQueries.single.key({ id: "1" }); // → ["users", "1"]
queryClient.invalidateQueries({ queryKey: usersQueries.base() }); // invalidates all users queriescreateQueryProxy
Wraps any typed object so every async method gets .$key() and .$query() helpers. Keys are derived automatically from the property-access path — no manual definitions needed:
import { createQueryProxy } from "@padosoft/utilities/lib/query-proxy";
const q = createQueryProxy(apiClient);
// Auto-derived key: ['v1', 'users', 'getUser', { id: '1' }]
useQuery({ ...q.v1.users.getUser.$query({ id: "1" }) });
// Direct call still works:
await q.v1.users.getUser({ id: "1" });Configuration store
A typed, observable config store with override support and React integration via @padosoft/react:
import { Configuration, defineConfigOverride } from "@padosoft/utilities/lib/configuration";
interface AppConfig {
apiUrl: string;
debug: boolean;
}
const config = new Configuration<AppConfig>({
apiUrl: "https://api.example.com",
debug: false,
});
// Read
config.get(); // full config
config.get("apiUrl"); // single field, typed
// Write (triggers all subscribers)
config.set({ apiUrl: "https://staging.example.com", debug: true });
// Overrides — applied on every set()
config.addOverride(
defineConfigOverride("apiUrl", (c) => c.debug ? "http://localhost:3000" : c.apiUrl),
);
// Subscribe (returns an unsubscribe fn)
const unsub = config.subscribe(() => console.log("config changed"));Zod utilities
import { formatZodError } from "@padosoft/utilities/lib/zod/utils";
import type { ConvertMaybeZod, DeepConvertMaybeZod } from "@padosoft/utilities/types/zod";
import { jsonCodec } from "@padosoft/utilities/lib/zod/codecs/json";formatZodError(error)— formats aZodErrorinto{ code, message }.ConvertMaybeZod<T>— ifTis a Zod schema, infers its output type; otherwise returnsTunchanged.DeepConvertMaybeZod<T>— same but recursively through objects and arrays.jsonCodec— Zod codec that serialises/parses JSON strings.
Type helpers
import type {
DeepPartial,
FullPartial,
Prettify,
Intersect,
MergeWithDefault,
Satisfies,
LiteralUnion,
Mutable,
DeepMutable,
} from "@padosoft/utilities/types/utils";| Type | Description |
|------|-------------|
| DeepPartial<T> | Recursively makes all properties optional |
| FullPartial<T> | Shallow optional including undefined |
| Prettify<T> | Flattens intersections for readable IDE output |
| Intersect<T> | Reduces a union to its intersection |
| MergeWithDefault<T, Key, Value> | Adds a default key/value if missing from T |
| Satisfies<U, T> | Compile-time satisfies as a type alias |
| LiteralUnion<T, U> | Literal union that also accepts the base type without widening |
| Mutable<T> / DeepMutable<T> | Removes readonly modifiers |
Misc utilities
import { fillArray, toPascalCase, isColor, sleep } from "@padosoft/utilities/lib/utils";| Function | Description |
|----------|-------------|
| fillArray(obj, n) | Creates an array of n shallow copies of obj |
| toPascalCase(str) | Converts kebab/snake/space strings to PascalCase |
| isColor(str) | Returns true if str is a valid hex or rgb color |
| sleep(ms) | Returns a Promise that resolves after ms milliseconds |
Turborepo utilities
import { listApps, listPacakges, fileNameValidator } from "@padosoft/utilities/lib/turbo";Helpers for Turborepo generators: list apps/packages in the monorepo and validate user-provided file names.
Zod defaults
import { withZodDefaults } from "@padosoft/utilities/lib/zod/defaults";Merges a partial defaults map into a Zod object schema so that missing fields use the given default values at parse time. Works with zod v4.
const schema = z.object({
theme: z.string(),
debug: z.boolean(),
retries: z.number(),
});
const withDefaults = withZodDefaults(schema, {
theme: "light",
retries: 3,
});
withDefaults.parse({});
// → { theme: "light", debug: <still required>, retries: 3 }Promise utilities
import { inspectSettledPromiseResults } from "@padosoft/utilities/lib/promise";Splits a Promise.allSettled result array into fulfilled values and rejected reasons:
const results = await Promise.allSettled([fetchUser(1), fetchUser(2), fetchUser(3)]);
const { fulfilled, rejected } = inspectSettledPromiseResults(results);
// fulfilled: User[] rejected: unknown[]Formatters
import { createCurrencyFormatter } from "@padosoft/utilities/lib/formatters";Creates an Intl.NumberFormat instance for currency display. Locale defaults to the runtime's default locale:
const eur = createCurrencyFormatter("EUR", "it-IT");
eur.format(1234.5); // → "1.234,50 €"
const usd = createCurrencyFormatter("USD");
usd.format(1234.5); // → "$1,234.50" (using runtime locale)