@mkvlrn/result 4.0.0 → 4.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @repo/result
2
+
3
+ ## 4.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 8ebb722: move to monorepo
package/README.md CHANGED
@@ -1,94 +1,54 @@
1
- # Result Pattern
2
-
3
- Type-safe Result pattern for TypeScript representing success or error. Anything to avoid try/catch hell.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm add @mkvlrn/result
9
- ```
10
-
11
- ## Usage
12
-
13
- ```typescript
14
- import { Result, AsyncResult, R } from "@mkvlrn/result";
15
-
16
- // Success
17
- const success = R.ok(42);
18
-
19
- // Error
20
- const failure = R.error(new Error("Something went wrong"));
21
-
22
- // Check result
23
- const result = R.ok(42);
24
- if (result.error) {
25
- console.log("Error:", result.error.message);
26
- } else {
27
- console.log("Value:", result.value);
28
- }
29
- ```
30
-
31
- ## Examples
32
-
33
- ### Basic Function
34
-
35
- ```typescript
36
- function divide(a: number, b: number): Result<number, Error> {
37
- if (b === 0) {
38
- return R.error(new Error("Division by zero"));
39
- }
40
- return R.ok(a / b);
41
- }
42
-
43
- const result = divide(10, 2);
44
- if (!result.error) {
45
- console.log(result.value); // 5
46
- }
47
- ```
48
-
49
- ### Async Operations
50
-
51
- ```typescript
52
- async function fetchUser(id: number): AsyncResult<User, Error> {
53
- try {
54
- const response = await fetch(`/api/users/${id}`);
55
- if (!response.ok) {
56
- return R.error(new Error(`HTTP ${response.status}`));
57
- }
58
- const user = await response.json();
59
- return R.ok(user);
60
- } catch (error) {
61
- return R.error(error instanceof Error ? error : new Error("Unknown error"));
62
- }
63
- }
64
- ```
65
-
66
- ### Custom Error Types
67
-
68
- ```typescript
69
- class ValidationError extends Error {
70
- readonly customField: number;
71
-
72
- constructor(customField: number, message: string) {
73
- super(message);
74
- this.name = "ValidationError";
75
- this.customField = customField;
76
- }
77
- }
78
-
79
- function validateEmail(email: string): Result<string, ValidationError> {
80
- if (!email.includes("@")) {
81
- return Result.error(new ValidationError(400, "custom"));
82
- }
83
- return Result.ok(email);
84
- }
85
-
86
- const result = validateEmail("invalid-email");
87
- if (result.error) {
88
- console.log(`${result.error.customField}: ${result.error.message}`);
89
- }
90
- ```
91
-
92
- ## License
93
-
94
- MIT
1
+ # template-node
2
+
3
+ A sane, opinionated template for esm node projects written in typescript.
4
+
5
+ For new, node 24+ projects.
6
+
7
+ Uses:
8
+
9
+ - [biome](https://github.com/biomejs/biome) for linting and formatting
10
+ - [commitlint](https://github.com/conventional-changelog/commitlint) for linting commit messages
11
+ - [husky](https://github.com/typicode/husky) for git hooks
12
+ - [vite](https://github.com/vitejs/vite) for building
13
+ - [vitest](https://github.com/vitest-dev/vitest) for testing
14
+ - [tsx](https://github.com/privatenumber/tsx) for dev time typescript
15
+
16
+ ## running
17
+
18
+ ### `npm run dev`
19
+
20
+ Runs the project in watch mode.
21
+
22
+ ### `npm run build`
23
+
24
+ Builds/transpiles the code to `./build`.
25
+
26
+ ### `npm start`
27
+
28
+ Runs the built project.
29
+
30
+ ### `npm test`
31
+
32
+ Runs tests.
33
+
34
+ ### `npm run biome-fix`
35
+
36
+ Runs biome in fix mode (only [safe fixes](https://biomejs.dev/linter/#safe-fixes)) to lint and format the project.
37
+
38
+ ### `npm run typecheck`
39
+
40
+ Runs type checking using tsc.
41
+
42
+ ## that tsconfig.json seems very strict and opinionated
43
+
44
+ Yup.
45
+
46
+ ## vscode
47
+
48
+ You might want to install the recommended extensions in vscode. Search for **@recommended** in the extensions tab, they'll show up as _"workspace recommendations"_.
49
+
50
+ If you have been using eslint and prettier and their extensions, you might want to disable eslint entirely and keep prettier as the formatter only for certain types of files.
51
+
52
+ This is done by the `.vscode/settings.json` file.
53
+
54
+ Debug configurations are also included (for source using tsx and for bundle using the generated source maps).
package/biome.jsonc ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
3
+ "root": false,
4
+ "extends": "//",
5
+ "files": {
6
+ "ignoreUnknown": true,
7
+ "includes": ["**/*", "!build/**", "!node_modules/**", "!dist/**", "!coverage/**"]
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,28 +1,45 @@
1
1
  {
2
2
  "name": "@mkvlrn/result",
3
- "description": "Result type for TypeScript",
4
- "version": "4.0.0",
3
+ "description": "Simple Result type/pattern for TypeScript",
4
+ "version": "4.0.2",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "publishConfig": {
8
- "access": "public",
9
- "registry": "https://registry.npmjs.org"
7
+ "author": "Mike Valeriano <mkvlrn@proton.me>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git@github.com:mkvlrn/tools"
10
11
  },
11
- "exports": {
12
- "types": "./build/index.d.ts",
13
- "default": "./build/index.js"
14
- },
15
- "files": [
16
- "build"
12
+ "keywords": [
13
+ "node",
14
+ "typescript",
15
+ "result"
17
16
  ],
17
+ "engines": {
18
+ "node": "24.x"
19
+ },
18
20
  "scripts": {
21
+ "build": "vite build",
22
+ "dev": "tsx --watch src/main.ts",
23
+ "start": "node build/bundle.js",
19
24
  "test": "vitest",
20
- "build": "rm -rf build && tsc"
25
+ "biome-check": "biome check --no-errors-on-unmatched",
26
+ "biome-fix": "npm run biome-check --write",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepare": "husky"
21
29
  },
22
30
  "devDependencies": {
23
- "@biomejs/biome": "^2.1.2",
31
+ "@biomejs/biome": "^2.1.3",
32
+ "@commitlint/cli": "^19.8.1",
33
+ "@commitlint/config-conventional": "^19.8.1",
24
34
  "@types/node": "^24.1.0",
25
- "typescript": "^5.8.3",
35
+ "@vitest/coverage-v8": "^3.2.4",
36
+ "husky": "^9.1.7",
37
+ "rollup-plugin-node-externals": "^8.0.1",
38
+ "tsx": "^4.20.3",
39
+ "typescript": "^5.9.2",
40
+ "vite": "^7.0.6",
41
+ "vite-plugin-dts": "^4.5.4",
42
+ "vite-tsconfig-paths": "^5.1.4",
26
43
  "vitest": "^3.2.4"
27
44
  }
28
45
  }
@@ -0,0 +1,66 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ import { assert, describe, it } from "vitest";
3
+ import { type AsyncResult, R, type Result } from "./index.js";
4
+
5
+ class CustomError extends Error {
6
+ readonly customField: number;
7
+
8
+ constructor(customField: number, message: string) {
9
+ super(message);
10
+ this.name = "CustomField";
11
+ this.customField = customField;
12
+ }
13
+ }
14
+
15
+ function division(a: number, b: number): Result<number, Error> {
16
+ if (b === 0) {
17
+ return R.error(new Error("cannot divide by zero"));
18
+ }
19
+
20
+ return R.ok(a / b);
21
+ }
22
+
23
+ async function longRunning(shouldFail: boolean): AsyncResult<number, CustomError> {
24
+ await setTimeout(1);
25
+
26
+ return shouldFail ? R.error(new CustomError(42, "wrong")) : R.ok(3);
27
+ }
28
+
29
+ describe("creates a valid result", () => {
30
+ describe("default Error type", () => {
31
+ it("ok result", () => {
32
+ const result = division(4, 2);
33
+
34
+ assert.isUndefined(result.error);
35
+ assert.isDefined(result.value);
36
+ assert.strictEqual(result.value, 2);
37
+ });
38
+
39
+ it("error result", () => {
40
+ const result = division(4, 0);
41
+
42
+ assert.isDefined(result.error);
43
+ assert.instanceOf(result.error, Error);
44
+ assert.strictEqual(result.error.message, "cannot divide by zero");
45
+ });
46
+ });
47
+
48
+ describe("custom error", () => {
49
+ it("ok result", async () => {
50
+ const result = await longRunning(false);
51
+
52
+ assert.isUndefined(result.error);
53
+ assert.isDefined(result.value);
54
+ assert.strictEqual(result.value, 3);
55
+ });
56
+
57
+ it("error result", async () => {
58
+ const result = await longRunning(true);
59
+
60
+ assert.isDefined(result.error);
61
+ assert.instanceOf(result.error, CustomError);
62
+ assert.strictEqual(result.error.message, "wrong");
63
+ assert.strictEqual(result.error.customField, 42);
64
+ });
65
+ });
66
+ });
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Result type to represent the outcome of an operation.
3
+ * It can either be a success with a value or an error.
4
+ * This is a generic type that can be used with any type of value and error (should extend Error).
5
+ *
6
+ * It is also an alias object containing the ok and error functions to
7
+ * make it easier to create Result objects.
8
+ */
9
+ export type Result<T, E extends Error> =
10
+ | { readonly error: undefined; readonly value: T }
11
+ | { readonly error: E };
12
+
13
+ /**
14
+ * Async version of Result type that wraps a Result in a Promise.
15
+ */
16
+ export type AsyncResult<T, E extends Error> = Promise<Result<T, E>>;
17
+
18
+ /**
19
+ * Result utility functions for creating Result objects.
20
+ */
21
+ export const R: {
22
+ /**
23
+ * Creates a successful Result with the given value.
24
+ * @param value The success value
25
+ * @returns A Result object representing success
26
+ */
27
+ ok<T>(value: T): Result<T, never>;
28
+ /**
29
+ * Creates an error Result with the given error.
30
+ * @param error The error value
31
+ * @returns A Result object representing error
32
+ */
33
+ error<E extends Error>(error: E): Result<never, E>;
34
+ } = {
35
+ ok: <T>(value: T): Result<T, never> => ({ error: undefined, value }),
36
+ error: <E extends Error>(error: E): Result<never, E> => ({ error }),
37
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "compilerOptions": {
3
+ // no transpiling
4
+ "noEmit": true,
5
+
6
+ // no funny business
7
+ "erasableSyntaxOnly": true,
8
+ "verbatimModuleSyntax": true,
9
+
10
+ // very strict
11
+ // https://whatislove.dev/articles/the-strictest-typescript-config/
12
+ "allowJs": false,
13
+ "exactOptionalPropertyTypes": true,
14
+ // "noPropertyAccessFromIndexSignature": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "strict": true,
17
+ "strictNullChecks": true,
18
+ "noUncheckedSideEffectImports": true,
19
+
20
+ // esm
21
+ "esModuleInterop": true,
22
+ "isolatedModules": true,
23
+ "lib": ["ESNext"],
24
+ "module": "esnext",
25
+ "moduleResolution": "bundler",
26
+ "target": "esnext",
27
+
28
+ // pnpm compatibility
29
+ "preserveSymlinks": true,
30
+
31
+ // self explanatory
32
+ "resolveJsonModule": true,
33
+
34
+ // don't try to check for errors on imported libs
35
+ "skipLibCheck": true,
36
+
37
+ // paths
38
+ "baseUrl": ".",
39
+ "paths": {
40
+ "#/*": ["./src/*"]
41
+ }
42
+ }
43
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { existsSync, globSync } from "node:fs";
2
+ import nodeExternals from "rollup-plugin-node-externals";
3
+ import dts from "vite-plugin-dts";
4
+ import tsconfigPaths from "vite-tsconfig-paths";
5
+ import { defineConfig } from "vitest/config";
6
+
7
+ // application entry point
8
+ const entry = globSync("./src/**/*.ts").filter((f) => !f.endsWith("test.ts"));
9
+ const entryRoot = "src";
10
+
11
+ // emits declarations only if there is no src/main.ts file
12
+ const dtsPlugin = existsSync("./src/main.ts")
13
+ ? null
14
+ : dts({ include: entry, logLevel: "error", entryRoot: entryRoot });
15
+
16
+ export default defineConfig({
17
+ plugins: [
18
+ // externalize node built-ins only
19
+ nodeExternals(),
20
+ // resolve tsconfig path aliases
21
+ tsconfigPaths(),
22
+ // declarations (if lib)
23
+ dtsPlugin,
24
+ ].filter(Boolean),
25
+
26
+ build: {
27
+ target: "esnext",
28
+ lib: {
29
+ entry,
30
+ formats: ["es"],
31
+ },
32
+ sourcemap: true,
33
+ outDir: "./build",
34
+ emptyOutDir: true,
35
+ rollupOptions: { output: { preserveModules: true, preserveModulesRoot: entryRoot } },
36
+ },
37
+
38
+ test: {
39
+ include: ["./src/**/*.test.{ts,tsx}"],
40
+ reporters: ["verbose"],
41
+ watch: false,
42
+ coverage: {
43
+ all: true,
44
+ clean: true,
45
+ cleanOnRerun: true,
46
+ include: ["src"],
47
+ exclude: ["**/*.test.{ts,tsx}", "**/*main.ts"],
48
+ },
49
+ // biome-ignore lint/style/useNamingConvention: needed for vitest
50
+ env: { NODE_ENV: "test" },
51
+ environment: "node",
52
+ passWithNoTests: true,
53
+ setupFiles: [],
54
+ },
55
+ });
package/build/index.d.ts DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Result type to represent the outcome of an operation.
3
- * It can either be a success with a value or an error.
4
- * This is a generic type that can be used with any type of value and error (should extend Error).
5
- *
6
- * It is also an alias object containing the ok and error functions to
7
- * make it easier to create Result objects.
8
- */
9
- export type Result<T, E extends Error> = {
10
- readonly error: undefined;
11
- readonly value: T;
12
- } | {
13
- readonly error: E;
14
- };
15
- /**
16
- * Async version of Result type that wraps a Result in a Promise.
17
- */
18
- export type AsyncResult<T, E extends Error> = Promise<Result<T, E>>;
19
- /**
20
- * Result utility functions for creating Result objects.
21
- */
22
- export declare const R: {
23
- /**
24
- * Creates a successful Result with the given value.
25
- * @param value The success value
26
- * @returns A Result object representing success
27
- */
28
- ok<T>(value: T): Result<T, never>;
29
- /**
30
- * Creates an error Result with the given error.
31
- * @param error The error value
32
- * @returns A Result object representing error
33
- */
34
- error<E extends Error>(error: E): Result<never, E>;
35
- };
package/build/index.js DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * Result utility functions for creating Result objects.
3
- */
4
- export const R = {
5
- ok: (value) => ({ error: undefined, value }),
6
- error: (error) => ({ error }),
7
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,53 +0,0 @@
1
- // biome-ignore lint/correctness/noNodejsModules: need the timer
2
- import { setTimeout } from "node:timers/promises";
3
- import { assert, describe, it } from "vitest";
4
- import { R } from "./index.js";
5
- class CustomError extends Error {
6
- customField;
7
- constructor(customField, message) {
8
- super(message);
9
- this.name = "CustomField";
10
- this.customField = customField;
11
- }
12
- }
13
- function division(a, b) {
14
- if (b === 0) {
15
- return R.error(new Error("cannot divide by zero"));
16
- }
17
- return R.ok(a / b);
18
- }
19
- async function longRunning(shouldFail) {
20
- await setTimeout(1);
21
- return shouldFail ? R.error(new CustomError(42, "wrong")) : R.ok(3);
22
- }
23
- describe("creates a valid result", () => {
24
- describe("default Error type", () => {
25
- it("ok result", () => {
26
- const result = division(4, 2);
27
- assert.isUndefined(result.error);
28
- assert.isDefined(result.value);
29
- assert.strictEqual(result.value, 2);
30
- });
31
- it("error result", () => {
32
- const result = division(4, 0);
33
- assert.isDefined(result.error);
34
- assert.instanceOf(result.error, Error);
35
- assert.strictEqual(result.error.message, "cannot divide by zero");
36
- });
37
- });
38
- describe("custom error", () => {
39
- it("ok result", async () => {
40
- const result = await longRunning(false);
41
- assert.isUndefined(result.error);
42
- assert.isDefined(result.value);
43
- assert.strictEqual(result.value, 3);
44
- });
45
- it("error result", async () => {
46
- const result = await longRunning(true);
47
- assert.isDefined(result.error);
48
- assert.instanceOf(result.error, CustomError);
49
- assert.strictEqual(result.error.message, "wrong");
50
- assert.strictEqual(result.error.customField, 42);
51
- });
52
- });
53
- });