@fulcro/functions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 diguu <rodrigogeribola@hotmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @fulcro/functions
2
+
3
+ Two runtime helpers that turn control flow into values. No dependencies, no
4
+ compiler involvement, nothing to configure.
5
+
6
+ ```ts
7
+ import { switchFor, tryCatch } from '@fulcro/functions';
8
+ ```
9
+
10
+ ## `switchFor`
11
+
12
+ Choosing between branches, as an expression rather than as a statement. It comes
13
+ in two forms, and which one to reach for depends on whether the value is drawn
14
+ from a closed set.
15
+
16
+ ### Exhaustive form — enums and literal unions
17
+
18
+ Pass one branch per member, and the compiler enforces that every member has one.
19
+
20
+ ```ts
21
+ enum Status {
22
+ Draft,
23
+ Published,
24
+ Archived,
25
+ }
26
+
27
+ const label = switchFor(status, {
28
+ [Status.Draft]: () => 'draft',
29
+ [Status.Published]: () => 'published',
30
+ [Status.Archived]: () => 'archived',
31
+ });
32
+ ```
33
+
34
+ Leave a member out and it does not compile:
35
+
36
+ ```
37
+ error TS2345: Property '[Status.Archived]' is missing in type
38
+ '{ 0: () => string; 1: () => string; }' but required in type
39
+ 'ExhaustiveCases<Status, string>'.
40
+ ```
41
+
42
+ **That error is the whole point.** When a member is added to the enum later,
43
+ every `switchFor` over it stops compiling until the new case is handled — which
44
+ is the moment to decide what it should do, rather than finding the gap in
45
+ production. A native `switch` says nothing in that situation; it just falls
46
+ through.
47
+
48
+ There is deliberately **no fallback parameter** in this form. A fallback is
49
+ precisely what would absorb the new member in silence and take the guarantee
50
+ away.
51
+
52
+ Each branch receives the single member it handles, already narrowed:
53
+
54
+ ```ts
55
+ switchFor(status, {
56
+ [Status.Draft]: (value) => {
57
+ const draft: Status.Draft = value; // not Status
58
+ return render(draft);
59
+ },
60
+ // …
61
+ });
62
+ ```
63
+
64
+ Works with numeric enums, string enums, and unions of string or number
65
+ literals. A member whose value is `0` is dispatched like any other — the branch
66
+ is found by key, never by testing the value for truthiness.
67
+
68
+ ### Running it for its effects
69
+
70
+ There is no separate void-returning function, and none is needed. Where the
71
+ branches return nothing, `R` is inferred as `void` and the call stands on its
72
+ own as a statement:
73
+
74
+ ```ts
75
+ switchFor(status, {
76
+ [Status.Draft]: () => saveDraft(),
77
+ [Status.Published]: () => publish(),
78
+ [Status.Archived]: () => archive(),
79
+ });
80
+ ```
81
+
82
+ The exhaustiveness check applies there exactly as it does to a call whose result
83
+ is read — leave a member out and this fails to compile too.
84
+
85
+ ### Predicate form — everything else
86
+
87
+ Where branches are conditions rather than values:
88
+
89
+ ```ts
90
+ const size = switchFor(
91
+ order,
92
+ [
93
+ { when: (o) => o.total > 1000, then: () => 'large' },
94
+ { when: (o) => o.items.length === 0, then: () => 'empty' },
95
+ ],
96
+ () => 'standard',
97
+ );
98
+ ```
99
+
100
+ Branches are tested in order, the first match wins, and the rest are never
101
+ evaluated — neither their conditions nor their bodies.
102
+
103
+ `otherwise` is optional, and leaving it out shows up in the type instead of
104
+ being hidden:
105
+
106
+ ```ts
107
+ const a = switchFor(n, cases); // R | undefined
108
+ const b = switchFor(n, cases, () => fallback); // R
109
+ ```
110
+
111
+ Without a fallback, an unmatched value evaluates to `undefined` and the compiler
112
+ makes you account for it. With one, the `undefined` is gone, because nothing can
113
+ produce it any more.
114
+
115
+ This form **cannot** be exhaustive. A condition is an arbitrary function, and
116
+ the compiler cannot reason about which values it accepts — which is why it takes
117
+ a fallback and the exhaustive form does not. It also decides a branch without
118
+ narrowing the value inside `then`.
119
+
120
+ ## `tryCatch`
121
+
122
+ The outcome of an operation as a value, instead of as control flow.
123
+
124
+ ```ts
125
+ const result = await tryCatch(() => fetch(url));
126
+
127
+ if (result.error !== null) return fallback;
128
+
129
+ use(result.data);
130
+ ```
131
+
132
+ **Prefer the callback form.** Passing a promise that already exists cannot catch
133
+ anything the expression throws on its way to producing it — in
134
+ `tryCatch(risky())`, `risky` runs first, and a synchronous throw inside it
135
+ escapes before `tryCatch` is ever called. The callback form moves that call
136
+ inside the `try`, which is the only way to cover both the synchronous and the
137
+ asynchronous failure of one operation. The promise form is still accepted, and
138
+ reads better when the promise is already in hand.
139
+
140
+ **Discriminate on `error`, never on `data`.** `0`, `''` and `null` are perfectly
141
+ good results, and `if (result.data)` reports every one of them as a failure.
142
+ Checking `result.error === null` narrows the union properly:
143
+
144
+ ```ts
145
+ if (result.error === null) {
146
+ result.data; // T, not T | null
147
+ }
148
+ ```
149
+
150
+ **`E` defaults to `unknown`, not to `Error`.** JavaScript lets any value be
151
+ thrown, so typing the error as an `Error` would be a claim this function cannot
152
+ keep — `result.error.message` would read `undefined` whenever something threw a
153
+ string. Narrow it at the use site, or pass the type explicitly when you own
154
+ every throw site:
155
+
156
+ ```ts
157
+ const result = await tryCatch<User, ApiError>(() => api.load(id));
158
+ ```
159
+
160
+ A thrown `null` or `undefined` — legal, however pathological — is wrapped in an
161
+ `Error` carrying the original value as its `cause`, because storing it as it
162
+ came would make the failure indistinguishable from a success.
163
+
164
+ `tryCatch` always returns a promise, including for a fully synchronous
165
+ operation.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public entry point of the package, matching the `main` and `types` fields of
3
+ * `package.json`.
4
+ *
5
+ * Listed one by one rather than re-exported wholesale, so that adding an export
6
+ * to a module below is never enough on its own to put it in front of consumers.
7
+ */
8
+ export { type ExhaustiveCases, type SwitchCase, switchFor } from './switchFor/index.js';
9
+ export { type Failure, type Result, type Success, tryCatch } from './tryCatch/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryCatch = exports.switchFor = void 0;
4
+ /**
5
+ * Public entry point of the package, matching the `main` and `types` fields of
6
+ * `package.json`.
7
+ *
8
+ * Listed one by one rather than re-exported wholesale, so that adding an export
9
+ * to a module below is never enough on its own to put it in front of consumers.
10
+ */
11
+ var switchFor_1 = require("./switchFor/index.js");
12
+ Object.defineProperty(exports, "switchFor", { enumerable: true, get: function () { return switchFor_1.switchFor; } });
13
+ var tryCatch_1 = require("./tryCatch/index.js");
14
+ Object.defineProperty(exports, "tryCatch", { enumerable: true, get: function () { return tryCatch_1.tryCatch; } });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * One branch of the predicate form of {@link switchFor}.
3
+ *
4
+ * @template T Type of the evaluated value.
5
+ * @template R Type produced by the branch.
6
+ */
7
+ export interface SwitchCase<T, R> {
8
+ /**
9
+ * Condition deciding whether this branch handles the value.
10
+ *
11
+ * @param value Value being evaluated.
12
+ * @returns `true` when this branch handles it.
13
+ */
14
+ readonly when: (value: T) => boolean;
15
+ /**
16
+ * Produces the result of this branch.
17
+ *
18
+ * Only called for the branch that matched, so a branch may do work that
19
+ * would be wrong or expensive for a value it does not handle.
20
+ *
21
+ * @param value Value being evaluated.
22
+ * @returns The result of the branch.
23
+ */
24
+ readonly then: (value: T) => R;
25
+ }
26
+ /**
27
+ * A branch for every member of a closed set of values.
28
+ *
29
+ * Written as a mapped type over the union rather than as an index signature,
30
+ * which is what makes the exhaustiveness a compile error: a missing member is a
31
+ * missing required property, and an unknown one has nothing to be assigned to.
32
+ *
33
+ * Each branch receives the single member it handles, not the whole union, so
34
+ * the value arrives already narrowed.
35
+ *
36
+ * @template T Union of the values being matched, typically an enum.
37
+ * @template R Type produced by every branch.
38
+ */
39
+ export type ExhaustiveCases<T extends PropertyKey, R> = {
40
+ readonly [K in T]: (value: K) => R;
41
+ };
42
+ /**
43
+ * Chooses between branches, as an expression.
44
+ *
45
+ * Comes in two forms, and which one to reach for depends on whether the value
46
+ * being matched is drawn from a closed set.
47
+ *
48
+ * **Exhaustive form** — for an enum, or any union of string or number literals.
49
+ * Pass one branch per member and the compiler enforces that every member has
50
+ * one:
51
+ *
52
+ * ```ts
53
+ * enum Status {
54
+ * Draft,
55
+ * Published,
56
+ * Archived,
57
+ * }
58
+ *
59
+ * const label = switchFor(status, {
60
+ * [Status.Draft]: () => 'draft',
61
+ * [Status.Published]: () => 'published',
62
+ * [Status.Archived]: () => 'archived',
63
+ * });
64
+ * ```
65
+ *
66
+ * Leaving a member out does not compile, and neither does adding a branch for
67
+ * something that is not a member. That is the point of this form: when a
68
+ * member is added to the enum later, every `switchFor` over it stops compiling
69
+ * until it is handled — which is exactly the moment to decide what it should
70
+ * do, rather than discovering the gap at runtime. There is deliberately no
71
+ * fallback parameter here, because a fallback is precisely what would absorb
72
+ * the new member in silence and take the guarantee away.
73
+ *
74
+ * **Predicate form** — for anything else, where branches are conditions rather
75
+ * than values:
76
+ *
77
+ * ```ts
78
+ * const size = switchFor(
79
+ * order,
80
+ * [
81
+ * { when: (o) => o.total > 1000, then: () => 'large' },
82
+ * { when: (o) => o.items.length === 0, then: () => 'empty' },
83
+ * ],
84
+ * () => 'standard',
85
+ * );
86
+ * ```
87
+ *
88
+ * Branches are tested in order and the first match wins; the rest are never
89
+ * evaluated, neither their conditions nor their bodies.
90
+ *
91
+ * `otherwise` is optional, and leaving it out is reflected in the type rather
92
+ * than hidden: the call then evaluates to `R | undefined`, so the compiler
93
+ * makes the caller account for the value that matched nothing. Passing a
94
+ * fallback removes the `undefined`, because nothing can produce it any more.
95
+ *
96
+ * The predicate form cannot be exhaustive. A condition is an arbitrary function
97
+ * and the compiler cannot reason about which values it accepts, which is why
98
+ * only this form has a fallback at all. It also decides a branch without
99
+ * narrowing the value inside `then`, where the exhaustive form hands each
100
+ * branch the single member it handles.
101
+ *
102
+ * **Using it for its effects needs no separate function.** Where the branches
103
+ * return nothing, `R` is inferred as `void` and the call stands on its own as a
104
+ * statement:
105
+ *
106
+ * ```ts
107
+ * switchFor(status, {
108
+ * [Status.Draft]: () => saveDraft(),
109
+ * [Status.Published]: () => publish(),
110
+ * [Status.Archived]: () => archive(),
111
+ * });
112
+ * ```
113
+ *
114
+ * The exhaustiveness check applies there exactly as it does to a call whose
115
+ * result is read.
116
+ *
117
+ * @template T Type of the evaluated value.
118
+ * @template R Type produced by every branch.
119
+ * @param value Value being evaluated.
120
+ * @param cases One branch per member, or branches tested in order.
121
+ * @param otherwise Produces the result when no branch matches. Predicate form
122
+ * only; omitting it admits `undefined` into the result.
123
+ * @returns The result of the branch that handled the value, the result of
124
+ * `otherwise`, or `undefined` when nothing matched and no fallback was given.
125
+ */
126
+ export declare function switchFor<T extends PropertyKey, R>(value: T, cases: ExhaustiveCases<T, R>): R;
127
+ export declare function switchFor<T, R>(value: T, cases: readonly SwitchCase<T, R>[], otherwise: (value: T) => R): R;
128
+ export declare function switchFor<T, R>(value: T, cases: readonly SwitchCase<T, R>[]): R | undefined;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.switchFor = switchFor;
4
+ function switchFor(value, cases, otherwise) {
5
+ if (!Array.isArray(cases)) {
6
+ return cases[value](value);
7
+ }
8
+ const matched = cases.find(({ when }) => when(value));
9
+ if (matched !== undefined)
10
+ return matched.then(value);
11
+ return otherwise === undefined ? undefined : otherwise(value);
12
+ }
@@ -0,0 +1,77 @@
1
+ /** Outcome of an operation that produced a value. */
2
+ export interface Success<T> {
3
+ /** Value the operation produced. */
4
+ readonly data: T;
5
+ /** Always `null`, which is what tells a success from a failure. */
6
+ readonly error: null;
7
+ }
8
+ /**
9
+ * Outcome of an operation that threw.
10
+ *
11
+ * The error is non nullable by construction, so that `error === null` is enough
12
+ * to tell the two cases apart. {@link tryCatch} upholds that at runtime: a
13
+ * thrown `null` or `undefined` — legal in JavaScript, however pathological — is
14
+ * wrapped in an `Error` rather than stored as is, because storing it would make
15
+ * a failure indistinguishable from a success.
16
+ *
17
+ * @template E Type of the captured error.
18
+ */
19
+ export interface Failure<E> {
20
+ /** Always `null`, since the operation produced no value. */
21
+ readonly data: null;
22
+ /** The captured error. */
23
+ readonly error: NonNullable<E>;
24
+ }
25
+ /**
26
+ * The outcome of an operation, as a value rather than as control flow.
27
+ *
28
+ * Discriminate on `error`, never on `data`:
29
+ *
30
+ * ```ts
31
+ * if (result.error === null) use(result.data);
32
+ * ```
33
+ *
34
+ * `data` is a valid discriminant only while the success type excludes every
35
+ * falsy value, which is a property of `T` and not of this type — `0`, `''` and
36
+ * `null` are all perfectly good results, and `if (result.data)` reports each of
37
+ * them as a failure.
38
+ *
39
+ * @template T Type produced on success.
40
+ * @template E Type of the error captured on failure.
41
+ */
42
+ export type Result<T, E = unknown> = Success<T> | Failure<E>;
43
+ /**
44
+ * Runs an operation and returns its outcome instead of throwing.
45
+ *
46
+ * ```ts
47
+ * const result = await tryCatch(() => fetch(url));
48
+ *
49
+ * if (result.error !== null) return fallback;
50
+ *
51
+ * use(result.data);
52
+ * ```
53
+ *
54
+ * **Prefer the callback form.** Passing a promise that already exists cannot
55
+ * catch anything the expression throws on its way to producing it: in
56
+ * `tryCatch(risky())`, `risky` runs first, and a synchronous throw inside it
57
+ * escapes before this function is ever called. The callback form moves that
58
+ * call inside the `try`, which is the only way to cover both the synchronous
59
+ * and the asynchronous failure of the same operation. The promise form is kept
60
+ * because it reads better when the promise is already in hand.
61
+ *
62
+ * `E` defaults to `unknown` rather than to `Error`, deliberately. JavaScript
63
+ * lets any value be thrown, and typing the error as an `Error` without checking
64
+ * would be a claim this function cannot keep — `result.error.message` would
65
+ * then read `undefined` whenever something threw a string. Narrow it at the use
66
+ * site, or pass the type explicitly when you own every throw site.
67
+ *
68
+ * Always returns a promise, including for an operation that is entirely
69
+ * synchronous.
70
+ *
71
+ * @template T Type produced by the operation.
72
+ * @template E Type of the error captured on failure.
73
+ * @param operation Callback performing the operation, or a promise already
74
+ * running it.
75
+ * @returns The outcome of the operation.
76
+ */
77
+ export declare const tryCatch: <T, E = unknown>(operation: Promise<T> | (() => T | PromiseLike<T>)) => Promise<Result<Awaited<T>, E>>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryCatch = void 0;
4
+ /**
5
+ * Something a thrown value can be stored as without defeating the discriminant.
6
+ *
7
+ * @param error Value that was thrown.
8
+ * @returns The value itself, or an `Error` standing in for a nullish throw.
9
+ */
10
+ const asStorableError = (error) => {
11
+ if (error !== null && error !== undefined)
12
+ return error;
13
+ return new Error(`Operation rejected with ${String(error)}`, {
14
+ cause: error,
15
+ });
16
+ };
17
+ /**
18
+ * Runs an operation and returns its outcome instead of throwing.
19
+ *
20
+ * ```ts
21
+ * const result = await tryCatch(() => fetch(url));
22
+ *
23
+ * if (result.error !== null) return fallback;
24
+ *
25
+ * use(result.data);
26
+ * ```
27
+ *
28
+ * **Prefer the callback form.** Passing a promise that already exists cannot
29
+ * catch anything the expression throws on its way to producing it: in
30
+ * `tryCatch(risky())`, `risky` runs first, and a synchronous throw inside it
31
+ * escapes before this function is ever called. The callback form moves that
32
+ * call inside the `try`, which is the only way to cover both the synchronous
33
+ * and the asynchronous failure of the same operation. The promise form is kept
34
+ * because it reads better when the promise is already in hand.
35
+ *
36
+ * `E` defaults to `unknown` rather than to `Error`, deliberately. JavaScript
37
+ * lets any value be thrown, and typing the error as an `Error` without checking
38
+ * would be a claim this function cannot keep — `result.error.message` would
39
+ * then read `undefined` whenever something threw a string. Narrow it at the use
40
+ * site, or pass the type explicitly when you own every throw site.
41
+ *
42
+ * Always returns a promise, including for an operation that is entirely
43
+ * synchronous.
44
+ *
45
+ * @template T Type produced by the operation.
46
+ * @template E Type of the error captured on failure.
47
+ * @param operation Callback performing the operation, or a promise already
48
+ * running it.
49
+ * @returns The outcome of the operation.
50
+ */
51
+ const tryCatch = async (operation) => {
52
+ try {
53
+ const data = await (typeof operation === 'function'
54
+ ? operation()
55
+ : operation);
56
+ return { data, error: null };
57
+ }
58
+ catch (error) {
59
+ return { data: null, error: asStorableError(error) };
60
+ }
61
+ };
62
+ exports.tryCatch = tryCatch;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@fulcro/functions",
3
+ "version": "0.1.0",
4
+ "description": "Runtime helpers turning control flow into values: an exhaustive switchFor and a throwing-free tryCatch.",
5
+ "keywords": [
6
+ "switch",
7
+ "exhaustive",
8
+ "enum",
9
+ "result",
10
+ "try-catch",
11
+ "typescript"
12
+ ],
13
+ "license": "ISC",
14
+ "author": "diguu <rodrigogeribola@hotmail.com>",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/DigUu-RL/fulcro.git",
37
+ "directory": "packages/functions"
38
+ },
39
+ "homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/functions#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/DigUu-RL/fulcro/issues"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
45
+ "typecheck": "tsc --noEmit -p tsconfig.json",
46
+ "prepublishOnly": "npm run build"
47
+ }
48
+ }