utilful 2.3.0 → 2.5.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/README.md CHANGED
@@ -13,7 +13,6 @@ A collection of TypeScript utilities that I use across my projects.
13
13
  - [JSON](#json)
14
14
  - [Module](#module)
15
15
  - [Object](#object)
16
- - [Path](#path)
17
16
  - [Result](#result)
18
17
  - [String](#string)
19
18
 
@@ -150,13 +149,7 @@ Recursively assign default properties. Simplified version based on [unjs/defu](h
150
149
 
151
150
  Recursively assigns missing properties from defaults to the source object. The source object takes precedence over defaults.
152
151
 
153
- **Key Features:**
154
-
155
- - **Nullish handling**: `null` and `undefined` values in source are replaced with defaults
156
- - **Array concatenation**: Arrays are concatenated (source + defaults)
157
- - **Deep merging**: Nested objects are recursively merged
158
- - **Type safety**: Preserves TypeScript types
159
- - **Prototype pollution protection**: Ignores `__proto__` and `constructor` keys
152
+ The function replaces `null` and `undefined` values in the source with defaults, concatenates arrays (source + defaults), and recursively merges nested objects.
160
153
 
161
154
  ```ts
162
155
  type PlainObject = Record<PropertyKey, any>
@@ -358,191 +351,170 @@ Deeply applies a callback to every key-value pair in the given object, as well a
358
351
  declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void
359
352
  ```
360
353
 
361
- ### Path
362
-
363
- #### `withoutLeadingSlash`
364
-
365
- Removes the leading slash from the given path if it has one.
366
-
367
- ```ts
368
- declare function withoutLeadingSlash(path?: string): string
369
- ```
370
-
371
- #### `withLeadingSlash`
354
+ ### Result
372
355
 
373
- Adds a leading slash to the given path if it does not already have one.
356
+ The `Result` type represents either success (`Ok`) or failure (`Err`). It provides a type-safe way to handle errors without relying on exceptions.
374
357
 
375
358
  ```ts
376
- declare function withLeadingSlash(path?: string): string
359
+ type Result<T, E> = Ok<T, E> | Err<T, E>
377
360
  ```
378
361
 
379
- #### `withoutTrailingSlash`
362
+ Both `Ok` and `Err` carry phantom types for proper type inference in unions.
380
363
 
381
- Removes the trailing slash from the given path if it has one.
364
+ **Basic example:**
382
365
 
383
366
  ```ts
384
- declare function withoutTrailingSlash(path?: string): string
385
- ```
386
-
387
- #### `withTrailingSlash`
367
+ import { err, ok } from 'utilful'
388
368
 
389
- Adds a trailing slash to the given path if it does not already have one.
369
+ function divide(a: number, b: number) {
370
+ if (b === 0) {
371
+ return err('Division by zero')
372
+ }
373
+ return ok(a / b)
374
+ }
390
375
 
391
- ```ts
392
- declare function withTrailingSlash(path?: string): string
376
+ const result = divide(10, 2)
377
+ if (result.ok)
378
+ console.log('Result:', result.value)
379
+ else
380
+ console.error('Error:', result.error)
393
381
  ```
394
382
 
395
- #### `joinURL`
396
-
397
- Joins the given URL path segments, ensuring that there is only one slash between them.
383
+ **Fluent chaining:**
398
384
 
399
385
  ```ts
400
- declare function joinURL(...paths: (string | undefined)[]): string
401
- ```
402
-
403
- #### `withBase`
386
+ import { toResult } from 'utilful'
404
387
 
405
- Adds the base path to the input path, if it is not already present.
406
-
407
- ```ts
408
- declare function withBase(input?: string, base?: string): string
388
+ const name = toResult(() => JSON.parse(jsonString))
389
+ .map(data => data.user)
390
+ .map(user => user.name)
391
+ .unwrapOr('Anonymous')
409
392
  ```
410
393
 
411
- #### `withoutBase`
394
+ #### `ok`
412
395
 
413
- Removes the base path from the input path, if it is present.
396
+ Creates a successful result.
414
397
 
415
398
  ```ts
416
- declare function withoutBase(input?: string, base?: string): string
399
+ declare function ok<T, E = never>(value: T): Ok<T, E>
417
400
  ```
418
401
 
419
- #### `getPathname`
402
+ #### `err`
420
403
 
421
- Returns the pathname of the given path, which is the path without the query string.
404
+ Creates an error result.
422
405
 
423
406
  ```ts
424
- declare function getPathname(path?: string): string
407
+ declare function err<T = never, E extends string = string>(error: E): Err<T, E>
408
+ declare function err<T = never, E = unknown>(error: E): Err<T, E>
425
409
  ```
426
410
 
427
- #### `withQuery`
411
+ #### `isOk` / `isErr`
428
412
 
429
- Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
413
+ Type guards for narrowing `Result` types.
430
414
 
431
415
  ```ts
432
- declare function withQuery(input: string, query?: QueryObject): string
416
+ declare function isOk<T, E>(result: Result<T, E>): result is Ok<T, E>
417
+ declare function isErr<T, E>(result: Result<T, E>): result is Err<T, E>
433
418
  ```
434
419
 
435
420
  **Example:**
436
421
 
437
422
  ```ts
438
- import { withQuery } from 'utilful'
439
-
440
- const url = withQuery('https://example.com', {
441
- foo: 'bar',
442
- // This key is omitted
443
- baz: undefined,
444
- // Object values are stringified
445
- baz: { qux: 'quux' }
446
- })
423
+ const result = toResult(() => JSON.parse(str))
424
+ if (isOk(result)) {
425
+ console.log(result.value) // TypeScript knows this is Ok
426
+ }
447
427
  ```
448
428
 
449
- ### Result
450
-
451
- The `Result` type that represents either success (`Ok`) or failure (`Err`). It helps to handle errors in a more explicit and type-safe way, without relying on exceptions.
429
+ #### `Result.map`
452
430
 
453
- A common use case for `Result` is error handling in functions that might fail. Here's an example of a function that divides two numbers and returns a `Result`:
431
+ Transforms the success value. No-op on `Err`.
454
432
 
455
433
  ```ts
456
- import { err, ok } from 'utilful'
457
-
458
- function divide(a: number, b: number) {
459
- if (b === 0) {
460
- return err('Division by zero')
461
- }
462
- return ok(a / b)
463
- }
464
-
465
- const result = divide(10, 2)
466
- if (result.ok)
467
- console.log('Result:', result.value)
468
- else
469
- console.error('Error:', result.error)
434
+ ok(2).map(x => x * 3) // Ok(6)
435
+ err('fail').map(x => x * 3) // Err('fail')
470
436
  ```
471
437
 
472
- #### `Result`
438
+ #### `Result.mapError`
473
439
 
474
- The `Result` type represents either success (`Ok`) or failure (`Err`).
440
+ Transforms the error value. No-op on `Ok`.
475
441
 
476
442
  ```ts
477
- type Result<T, E> = Ok<T> | Err<E>
443
+ err('fail').mapError(e => e.toUpperCase()) // Err('FAIL')
444
+ ok(42).mapError(e => e.toUpperCase()) // Ok(42)
478
445
  ```
479
446
 
480
- #### `Ok`
447
+ #### `Result.andThen`
481
448
 
482
- The `Ok` type wraps a successful value.
483
-
484
- **Example:**
449
+ Chains a function that returns a `Result`. Useful for composing fallible operations.
485
450
 
486
451
  ```ts
487
- const result = new Ok(42)
452
+ ok(2).andThen(x => x > 0 ? ok(x) : err('negative')) // Ok(2)
453
+ err('fail').andThen(x => ok(x * 2)) // Err('fail') - short-circuits
488
454
  ```
489
455
 
490
- #### `Err`
456
+ #### `Result.unwrap`
491
457
 
492
- The `Err` type wraps an error value.
493
-
494
- **Example:**
458
+ Extracts the value or throws an error.
495
459
 
496
460
  ```ts
497
- const result = new Err('Something went wrong')
461
+ ok(42).unwrap() // 42
462
+ err('fail').unwrap() // throws Error
463
+ err('fail').unwrap('custom message') // throws Error('custom message')
498
464
  ```
499
465
 
500
- #### `ok`
466
+ #### `Result.unwrapOr`
501
467
 
502
- Shorthand function to create an `Ok` result. Use it to wrap a successful value.
468
+ Extracts the value or returns a fallback.
503
469
 
504
470
  ```ts
505
- declare function ok<T>(value: T): Ok<T>
471
+ ok(42).unwrapOr(0) // 42
472
+ err('fail').unwrapOr(0) // 0
506
473
  ```
507
474
 
508
- #### `err`
475
+ #### `Result.match`
509
476
 
510
- Shorthand function to create an `Err` result. Use it to wrap an error value.
477
+ Pattern matches on the result.
511
478
 
512
479
  ```ts
513
- declare function err<E extends string = string>(err: E): Err<E>
514
- declare function err<E = unknown>(err: E): Err<E>
480
+ result.match({
481
+ ok: value => `Success: ${value}`,
482
+ err: error => `Error: ${error}`,
483
+ })
515
484
  ```
516
485
 
517
486
  #### `toResult`
518
487
 
519
- Wraps a function that might throw an error and returns a `Result` with the result of the function.
488
+ Wraps a function or promise that might throw and returns a `Result`.
520
489
 
521
490
  ```ts
522
491
  declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
523
492
  declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
524
493
  ```
525
494
 
526
- #### `unwrapResult`
527
-
528
- Unwraps a `Result`, `Ok`, or `Err` value and returns the value or error in an object. If the result is an `Ok`, the object contains the value and an `undefined` error. If the result is an `Err`, the object contains an `undefined` value and the error.
495
+ **Example:**
529
496
 
530
497
  ```ts
531
- declare function unwrapResult<T>(result: Ok<T>): { value: T, error: undefined }
532
- declare function unwrapResult<E>(result: Err<E>): { value: undefined, error: E }
533
- declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
498
+ // Synchronous
499
+ const result = toResult(() => JSON.parse('{"foo":"bar"}'))
500
+
501
+ // Asynchronous
502
+ const result = await toResult(fetch('https://api.example.com'))
534
503
  ```
535
504
 
536
- **Example:**
505
+ #### `unwrapResult`
506
+
507
+ Converts a `Result` to a plain object with `value` and `error` properties.
537
508
 
538
509
  ```ts
539
- const result = toResult(() => JSON.parse('{"foo":"bar"}'))
540
- const { value, error } = unwrapResult(result)
510
+ declare function unwrapResult<T, E>(result: Ok<T, E>): { value: T, error: undefined }
511
+ declare function unwrapResult<T, E>(result: Err<T, E>): { value: undefined, error: E }
512
+ declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
541
513
  ```
542
514
 
543
515
  #### `tryCatch`
544
516
 
545
- A simpler alternative to `toResult` + `unwrapResult`. It executes a function that might throw an error and directly returns the result in a `ResultData` format. Works with both synchronous functions and promises.
517
+ Combines `toResult` and `unwrapResult` into one step. Executes a function and returns `{ value, error }` directly.
546
518
 
547
519
  ```ts
548
520
  declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
@@ -552,13 +524,11 @@ declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value:
552
524
  **Example:**
553
525
 
554
526
  ```ts
555
- import { tryCatch } from 'utilful'
556
-
557
- // Synchronous usage
527
+ // Synchronous
558
528
  const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
559
529
 
560
- // Asynchronous usage
561
- const { value, error } = await tryCatch(fetch('https://api.example.com/data').then(r => r.json()))
530
+ // Asynchronous
531
+ const { value, error } = await tryCatch(fetch('https://api.example.com').then(r => r.json()))
562
532
  ```
563
533
 
564
534
  ### String
package/dist/csv.d.mts CHANGED
@@ -55,6 +55,41 @@ interface CSVCreateOptions {
55
55
  declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], columns: readonly (keyof T)[], options?: CSVCreateOptions): string;
56
56
  declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], options?: CSVCreateOptions): string;
57
57
  /**
58
+ * Creates a CSV stream from an iterable or async iterable of objects.
59
+ *
60
+ * @remarks
61
+ * This function yields CSV content as strings, including line endings.
62
+ * Each yielded chunk contains complete lines (header and/or data rows).
63
+ *
64
+ * @example
65
+ * const data = [
66
+ * { name: 'John', age: '30' },
67
+ * { name: 'Jane', age: '25' }
68
+ * ]
69
+ *
70
+ * for await (const chunk of createCSVStream(data, ['name', 'age'])) {
71
+ * console.log(chunk)
72
+ * }
73
+ */
74
+ declare function createCSVStream<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): AsyncIterable<string>;
75
+ /**
76
+ * Creates a CSV string from an async iterable or iterable of objects.
77
+ *
78
+ * @remarks
79
+ * This is a convenience wrapper around `createCSVStream` that collects
80
+ * all chunks into a single string. Note that the result will have a
81
+ * trailing line ending, unlike the synchronous `createCSV`.
82
+ *
83
+ * @example
84
+ * const data = [
85
+ * { name: 'John', age: '30' },
86
+ * { name: 'Jane', age: '25' }
87
+ * ]
88
+ *
89
+ * const csv = await createCSVAsync(data, ['name', 'age'])
90
+ */
91
+ declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): Promise<string>;
92
+ /**
58
93
  * Escapes a value for a CSV string.
59
94
  *
60
95
  * @remarks
@@ -100,5 +135,45 @@ declare function parseCSV<Header extends string>(csv?: string | null | undefined
100
135
  */
101
136
  strict?: boolean;
102
137
  }): CSVRow<Header>[];
138
+ /**
139
+ * Parses CSV data from an async iterable or iterable of string chunks.
140
+ *
141
+ * @remarks
142
+ * This function yields CSV rows as they are parsed. Chunks do not need to
143
+ * align with row boundaries; the parser handles quotes and newlines correctly
144
+ * across chunk boundaries.
145
+ *
146
+ * @example
147
+ * const chunks = ['name,age\nJo', 'hn,30\nJane,25']
148
+ *
149
+ * for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
150
+ * console.log(row)
151
+ * }
152
+ */
153
+ declare function parseCSVStream<Header extends string>(chunks: AsyncIterable<string> | Iterable<string>, options?: {
154
+ delimiter?: string;
155
+ trim?: boolean;
156
+ strict?: boolean;
157
+ }): AsyncIterable<CSVRow<Header>>;
158
+ /**
159
+ * Parses CSV data from an async iterable or iterable of lines.
160
+ *
161
+ * @remarks
162
+ * This is a convenience wrapper around `parseCSVStream` that treats each line
163
+ * as a chunk. Note that lines do not necessarily correspond to CSV rows due to
164
+ * quoted fields containing newlines. The parser handles this correctly.
165
+ *
166
+ * @example
167
+ * const lines = ['name,age', 'John,30', 'Jane,25']
168
+ *
169
+ * for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
170
+ * console.log(row)
171
+ * }
172
+ */
173
+ declare function parseCSVFromLines<Header extends string>(lines: AsyncIterable<string> | Iterable<string>, options?: {
174
+ delimiter?: string;
175
+ trim?: boolean;
176
+ strict?: boolean;
177
+ }): AsyncIterable<CSVRow<Header>>;
103
178
  //#endregion
104
- export { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV };
179
+ export { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
package/dist/csv.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  //#region src/csv.ts
2
+ const COMMA = ",";
3
+ const DOUBLE_QUOTE = "\"";
4
+ const NEWLINE = "\n";
5
+ const CARRIAGE_RETURN = "\r";
6
+ const ESCAPED_QUOTE = "\"\"";
7
+ const SPACE = " ";
8
+ const TAB = " ";
2
9
  function createCSV(data, columnsOrOptions, maybeOptions = {}) {
3
10
  let columns;
4
11
  let options;
@@ -10,19 +17,71 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
10
17
  options = columnsOrOptions ?? {};
11
18
  }
12
19
  if (columns.length === 0 && data.length === 0) return "";
13
- const { delimiter = ",", addHeader = true, quoteAll = false, lineEnding = "\n" } = options;
20
+ const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
14
21
  if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
15
- const formatCell = (value) => escapeCSVValue(value, {
16
- delimiter,
17
- quoteAll
18
- });
19
- const rows = data.map((obj) => columns.map((key) => formatCell(obj[key])).join(delimiter));
20
22
  if (addHeader) {
21
- const header = columns.map(formatCell).join(delimiter);
22
- if (rows.length === 0) return header;
23
- return header + lineEnding + rows.join(lineEnding);
23
+ const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
24
+ if (data.length === 0) return header;
25
+ const bodyLines = data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll));
26
+ return header + lineEnding + bodyLines.join(lineEnding);
24
27
  }
25
- return rows.join(lineEnding);
28
+ return data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll)).join(lineEnding);
29
+ }
30
+ /**
31
+ * Creates a CSV stream from an iterable or async iterable of objects.
32
+ *
33
+ * @remarks
34
+ * This function yields CSV content as strings, including line endings.
35
+ * Each yielded chunk contains complete lines (header and/or data rows).
36
+ *
37
+ * @example
38
+ * const data = [
39
+ * { name: 'John', age: '30' },
40
+ * { name: 'Jane', age: '25' }
41
+ * ]
42
+ *
43
+ * for await (const chunk of createCSVStream(data, ['name', 'age'])) {
44
+ * console.log(chunk)
45
+ * }
46
+ */
47
+ async function* createCSVStream(data, columns, options = {}) {
48
+ const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
49
+ if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
50
+ if (addHeader) yield encodeCSVHeader(columns.map(String), delimiter, quoteAll) + lineEnding;
51
+ for await (const row of data) yield encodeCSVRow(row, columns, delimiter, quoteAll) + lineEnding;
52
+ }
53
+ /**
54
+ * Creates a CSV string from an async iterable or iterable of objects.
55
+ *
56
+ * @remarks
57
+ * This is a convenience wrapper around `createCSVStream` that collects
58
+ * all chunks into a single string. Note that the result will have a
59
+ * trailing line ending, unlike the synchronous `createCSV`.
60
+ *
61
+ * @example
62
+ * const data = [
63
+ * { name: 'John', age: '30' },
64
+ * { name: 'Jane', age: '25' }
65
+ * ]
66
+ *
67
+ * const csv = await createCSVAsync(data, ['name', 'age'])
68
+ */
69
+ async function createCSVAsync(data, columns, options = {}) {
70
+ const chunks = [];
71
+ for await (const chunk of createCSVStream(data, columns, options)) chunks.push(chunk);
72
+ return chunks.join("");
73
+ }
74
+ function encodeCSVHeader(columns, delimiter, quoteAll) {
75
+ return columns.map((col) => escapeCSVValue(col, {
76
+ delimiter,
77
+ quoteAll
78
+ })).join(delimiter);
79
+ }
80
+ function encodeCSVRow(row, columns, delimiter, quoteAll) {
81
+ return columns.map((key) => escapeCSVValue(row[key], {
82
+ delimiter,
83
+ quoteAll
84
+ })).join(delimiter);
26
85
  }
27
86
  /**
28
87
  * Escapes a value for a CSV string.
@@ -37,12 +96,118 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
37
96
  * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
38
97
  */
39
98
  function escapeCSVValue(value, options = {}) {
40
- const { delimiter = ",", quoteAll = false } = options;
99
+ const { delimiter = COMMA, quoteAll = false } = options;
41
100
  if (value == null) return "";
42
101
  const coercedValue = String(value);
43
- if (quoteAll || coercedValue.includes(delimiter) || coercedValue.includes("\"") || coercedValue.includes("\n") || coercedValue.includes("\r")) return `"${coercedValue.replaceAll("\"", "\"\"")}"`;
102
+ const hasQuote = coercedValue.includes(DOUBLE_QUOTE);
103
+ if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) return `${DOUBLE_QUOTE}${hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue}${DOUBLE_QUOTE}`;
44
104
  return coercedValue;
45
105
  }
106
+ var CSVParserCore = class {
107
+ delimiter;
108
+ trim;
109
+ strict;
110
+ onRow;
111
+ currentRow = [];
112
+ currentRowQuotedFlags = [];
113
+ currentField = "";
114
+ inQuotes = false;
115
+ isFieldQuoted = false;
116
+ currentRowNumber = 1;
117
+ headerRaw;
118
+ headerQuotedFlags;
119
+ headers;
120
+ constructor(options, onRow) {
121
+ const { delimiter = COMMA, trim = true, strict = true } = options;
122
+ if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
123
+ this.delimiter = delimiter;
124
+ this.trim = trim;
125
+ this.strict = strict;
126
+ this.onRow = onRow;
127
+ }
128
+ push(chunk) {
129
+ for (let i = 0; i < chunk.length; i++) {
130
+ const character = chunk[i];
131
+ const nextCharacter = i + 1 < chunk.length ? chunk[i + 1] : "";
132
+ if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
133
+ if (character === DOUBLE_QUOTE) if (this.currentField.length === 0 && !this.inQuotes) {
134
+ this.inQuotes = true;
135
+ this.isFieldQuoted = true;
136
+ } else if (this.inQuotes && nextCharacter === DOUBLE_QUOTE) {
137
+ this.currentField += DOUBLE_QUOTE;
138
+ i++;
139
+ } else if (this.inQuotes) this.inQuotes = false;
140
+ else this.currentField += character;
141
+ else if (character === this.delimiter && !this.inQuotes) this.appendField();
142
+ else if ((character === NEWLINE || character === CARRIAGE_RETURN) && !this.inQuotes) {
143
+ if (character === CARRIAGE_RETURN && nextCharacter === NEWLINE) i++;
144
+ this.appendRow();
145
+ } else this.currentField += character;
146
+ }
147
+ }
148
+ finish() {
149
+ if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
150
+ if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
151
+ }
152
+ appendField() {
153
+ this.currentRow.push(this.currentField);
154
+ this.currentRowQuotedFlags.push(this.isFieldQuoted);
155
+ this.currentField = "";
156
+ this.isFieldQuoted = false;
157
+ }
158
+ appendRow() {
159
+ this.appendField();
160
+ if (!this.headerRaw) {
161
+ this.headerRaw = this.currentRow;
162
+ this.headerQuotedFlags = this.currentRowQuotedFlags;
163
+ this.processHeaderRow();
164
+ } else this.processDataRow(this.currentRow, this.currentRowQuotedFlags);
165
+ this.currentRow = [];
166
+ this.currentRowQuotedFlags = [];
167
+ this.currentRowNumber++;
168
+ }
169
+ processHeaderRow() {
170
+ const headerRow = this.headerRaw;
171
+ const headerQuotedFlags = this.headerQuotedFlags ?? [];
172
+ const headers = this.trim ? headerRow.map((h, i) => headerQuotedFlags[i] ? h : h.trim()) : headerRow;
173
+ if (headers.filter((h) => h.length === 0).length > 0) {
174
+ const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
175
+ throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
176
+ }
177
+ const headerSet = /* @__PURE__ */ new Set();
178
+ const duplicateHeaderNames = /* @__PURE__ */ new Set();
179
+ for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
180
+ else headerSet.add(header);
181
+ if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
182
+ this.headers = headers;
183
+ }
184
+ processDataRow(fieldValues, quotedFlags) {
185
+ if (!this.headers) throw new Error("CSVParserCore: headers not initialized");
186
+ const headers = this.headers;
187
+ const isFieldPopulated = this.trim ? (field, wasQuoted) => wasQuoted ? field.length > 0 : field.trim().length > 0 : (field) => field.length > 0;
188
+ const hasMultipleFields = fieldValues.length > 1;
189
+ const hasAnyPopulatedField = fieldValues.some((field, idx) => isFieldPopulated(field, quotedFlags[idx] ?? false));
190
+ if (!hasMultipleFields && !hasAnyPopulatedField) return;
191
+ if (fieldValues.length > headers.length) {
192
+ const fieldsExceedingHeaders = fieldValues.slice(headers.length);
193
+ const excessQuotedFlags = quotedFlags.slice(headers.length);
194
+ const containsNonEmptyOverflow = fieldsExceedingHeaders.some((field, idx) => isFieldPopulated(field, excessQuotedFlags[idx] ?? false));
195
+ if (this.strict && containsNonEmptyOverflow) {
196
+ const expectedCount = headers.length;
197
+ const actualCount = fieldValues.length;
198
+ const excessCount = actualCount - expectedCount;
199
+ throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
200
+ }
201
+ }
202
+ const rowEntries = headers.map((header, columnIndex) => {
203
+ const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
204
+ const wasQuoted = quotedFlags[columnIndex] ?? false;
205
+ return [header, this.trim && !wasQuoted ? untrimmedValue.trim() : untrimmedValue];
206
+ });
207
+ const rowObject = Object.fromEntries(rowEntries);
208
+ this.onRow(rowObject);
209
+ }
210
+ };
46
211
  /**
47
212
  * Parses a comma-separated values (CSV) string into an array of objects.
48
213
  *
@@ -60,66 +225,57 @@ function escapeCSVValue(value, options = {}) {
60
225
  function parseCSV(csv, options = {}) {
61
226
  if (!csv?.trim()) return [];
62
227
  const rows = [];
63
- let currentRow = [];
64
- let currentField = "";
65
- let inQuotes = false;
66
- let currentRowNumber = 1;
67
- const { delimiter = ",", trim = true, strict = true } = options;
68
- if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
69
- const appendField = () => {
70
- currentRow.push(currentField);
71
- currentField = "";
72
- };
73
- const appendRow = () => {
74
- appendField();
75
- rows.push(currentRow);
76
- currentRow = [];
77
- };
78
- for (let i = 0; i < csv.length; i++) {
79
- const character = csv[i];
80
- const nextCharacter = i + 1 < csv.length ? csv[i + 1] : "";
81
- if (character === "\"") if (inQuotes && nextCharacter === "\"") {
82
- currentField += "\"";
83
- i++;
84
- } else inQuotes = !inQuotes;
85
- else if (character === delimiter && !inQuotes) appendField();
86
- else if ((character === "\n" || character === "\r" && nextCharacter === "\n") && !inQuotes) {
87
- if (character === "\r") i++;
88
- appendRow();
89
- currentRowNumber++;
90
- } else currentField += character;
91
- }
92
- if (currentField || currentRow.length > 0) appendRow();
93
- if (inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${currentRowNumber}`);
94
- if (rows.length <= 1) return [];
95
- const [headerRow] = rows;
96
- if (!headerRow) return [];
97
- const headers = trim ? headerRow.map((h) => h.trim()) : headerRow;
98
- if (headers.filter((h) => h.length === 0).length > 0) {
99
- const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
100
- throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
101
- }
102
- const headerSet = /* @__PURE__ */ new Set();
103
- const duplicateHeaderNames = /* @__PURE__ */ new Set();
104
- for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
105
- else headerSet.add(header);
106
- if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
107
- const isFieldPopulated = trim ? (field) => field.trim().length > 0 : (field) => field.length > 0;
108
- return rows.slice(1).filter((row) => row.length > 1 || row.some(isFieldPopulated)).map((fieldValues, rowIndex) => {
109
- if (fieldValues.length > headers.length) {
110
- const containsNonEmptyOverflow = fieldValues.slice(headers.length).some(isFieldPopulated);
111
- if (strict && containsNonEmptyOverflow) {
112
- const expectedCount = headers.length;
113
- const actualCount = fieldValues.length;
114
- const excessCount = actualCount - expectedCount;
115
- throw new SyntaxError(`CSV row ${rowIndex + 2} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
116
- }
117
- }
118
- return Object.fromEntries(headers.map((header, columnIndex) => {
119
- const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
120
- return [header, trim ? untrimmedValue.trim() : untrimmedValue];
121
- }));
228
+ const parser = new CSVParserCore(options, (row) => {
229
+ rows.push(row);
122
230
  });
231
+ parser.push(csv);
232
+ parser.finish();
233
+ return rows;
234
+ }
235
+ /**
236
+ * Parses CSV data from an async iterable or iterable of string chunks.
237
+ *
238
+ * @remarks
239
+ * This function yields CSV rows as they are parsed. Chunks do not need to
240
+ * align with row boundaries; the parser handles quotes and newlines correctly
241
+ * across chunk boundaries.
242
+ *
243
+ * @example
244
+ * const chunks = ['name,age\nJo', 'hn,30\nJane,25']
245
+ *
246
+ * for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
247
+ * console.log(row)
248
+ * }
249
+ */
250
+ async function* parseCSVStream(chunks, options = {}) {
251
+ const queue = [];
252
+ const parser = new CSVParserCore(options, (row) => {
253
+ queue.push(row);
254
+ });
255
+ for await (const chunk of chunks) {
256
+ parser.push(chunk);
257
+ while (queue.length > 0) yield queue.shift();
258
+ }
259
+ parser.finish();
260
+ while (queue.length > 0) yield queue.shift();
261
+ }
262
+ /**
263
+ * Parses CSV data from an async iterable or iterable of lines.
264
+ *
265
+ * @remarks
266
+ * This is a convenience wrapper around `parseCSVStream` that treats each line
267
+ * as a chunk. Note that lines do not necessarily correspond to CSV rows due to
268
+ * quoted fields containing newlines. The parser handles this correctly.
269
+ *
270
+ * @example
271
+ * const lines = ['name,age', 'John,30', 'Jane,25']
272
+ *
273
+ * for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
274
+ * console.log(row)
275
+ * }
276
+ */
277
+ async function* parseCSVFromLines(lines, options) {
278
+ yield* parseCSVStream(lines, options);
123
279
  }
124
280
  /**
125
281
  * Infers column names from data by collecting the union of keys
@@ -137,4 +293,4 @@ function inferColumns(rows) {
137
293
  }
138
294
 
139
295
  //#endregion
140
- export { createCSV, escapeCSVValue, parseCSV };
296
+ export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
package/dist/index.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { MaybeArray, toArray } from "./array.mjs";
2
- import { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
2
+ import { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
3
3
  import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
4
4
  import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
5
5
  import { cloneJSON, tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
8
8
  import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
- import { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
9
+ import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
10
  import { generateRandomId, template } from "./string.mjs";
11
11
  import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
12
- export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
12
+ export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { toArray } from "./array.mjs";
2
- import { createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
2
+ import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
3
3
  import { createDefu, defu } from "./defu.mjs";
4
4
  import { createEmitter } from "./emitter.mjs";
5
5
  import { cloneJSON, tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
8
8
  import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
- import { Err, Ok, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
9
+ import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
10
  import { generateRandomId, template } from "./string.mjs";
11
11
 
12
- export { Err, Ok, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
12
+ export { Err, Ok, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/json.mjs CHANGED
@@ -21,11 +21,11 @@ function tryParseJSON(value) {
21
21
  */
22
22
  function cloneJSON(value) {
23
23
  if (typeof value !== "object" || value === null) return value;
24
- if (Array.isArray(value)) return value.map((e) => typeof e !== "object" || e === null ? e : cloneJSON(e));
24
+ if (Array.isArray(value)) return value.map((element) => typeof element !== "object" || element === null ? element : cloneJSON(element));
25
25
  const result = {};
26
- for (const k in value) {
27
- const v = value[k];
28
- result[k] = typeof v !== "object" || v === null ? v : cloneJSON(v);
26
+ for (const key in value) {
27
+ const propertyValue = value[key];
28
+ result[key] = typeof propertyValue !== "object" || propertyValue === null ? propertyValue : cloneJSON(propertyValue);
29
29
  }
30
30
  return result;
31
31
  }
package/dist/path.d.mts CHANGED
@@ -10,11 +10,13 @@ declare function withoutLeadingSlash(path?: string): string;
10
10
  */
11
11
  declare function withLeadingSlash(path?: string): string;
12
12
  /**
13
- * Removes the trailing slash from the given path if it has one.
13
+ * Removes the trailing slash from the given path if it has one,
14
+ * preserving query strings and hash fragments.
14
15
  */
15
16
  declare function withoutTrailingSlash(path?: string): string;
16
17
  /**
17
- * Adds a trailing slash to the given path if it does not already have one
18
+ * Adds a trailing slash to the given path if it does not already have one,
19
+ * preserving query strings and hash fragments.
18
20
  */
19
21
  declare function withTrailingSlash(path?: string): string;
20
22
  /**
@@ -30,7 +32,7 @@ declare function withBase(input?: string, base?: string): string;
30
32
  */
31
33
  declare function withoutBase(input?: string, base?: string): string;
32
34
  /**
33
- * Returns the pathname of the given path, which is the path without the query string.
35
+ * Returns the pathname of the given path, which is the path without the query string or hash.
34
36
  */
35
37
  declare function getPathname(path?: string): string;
36
38
  /**
package/dist/path.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * Removes the leading slash from the given path if it has one.
4
4
  */
5
5
  function withoutLeadingSlash(path) {
6
- if (!path || path === "/") return "/";
6
+ if (!path) return "";
7
7
  return path[0] === "/" ? path.slice(1) : path;
8
8
  }
9
9
  /**
@@ -14,31 +14,48 @@ function withLeadingSlash(path) {
14
14
  return path[0] === "/" ? path : `/${path}`;
15
15
  }
16
16
  /**
17
- * Removes the trailing slash from the given path if it has one.
17
+ * Removes the trailing slash from the given path if it has one,
18
+ * preserving query strings and hash fragments.
18
19
  */
19
20
  function withoutTrailingSlash(path) {
20
21
  if (!path || path === "/") return "/";
21
- return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
22
+ let pathEnd = path.length;
23
+ const queryIndex = path.indexOf("?");
24
+ const hashIndex = path.indexOf("#");
25
+ if (queryIndex !== -1) pathEnd = queryIndex;
26
+ if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
27
+ if (pathEnd > 0 && path[pathEnd - 1] === "/") {
28
+ if (pathEnd === 1) return path;
29
+ return path.slice(0, pathEnd - 1) + path.slice(pathEnd);
30
+ }
31
+ return path;
22
32
  }
23
33
  /**
24
- * Adds a trailing slash to the given path if it does not already have one
34
+ * Adds a trailing slash to the given path if it does not already have one,
35
+ * preserving query strings and hash fragments.
25
36
  */
26
37
  function withTrailingSlash(path) {
27
38
  if (!path || path === "/") return "/";
28
- return path[path.length - 1] === "/" ? path : `${path}/`;
39
+ let pathEnd = path.length;
40
+ const queryIndex = path.indexOf("?");
41
+ const hashIndex = path.indexOf("#");
42
+ if (queryIndex !== -1) pathEnd = queryIndex;
43
+ if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
44
+ if (pathEnd > 0 && path[pathEnd - 1] === "/") return path;
45
+ return `${path.slice(0, pathEnd)}/${path.slice(pathEnd)}`;
29
46
  }
30
47
  /**
31
48
  * Joins the given URL path segments, ensuring that there is only one slash between them.
32
49
  */
33
50
  function joinURL(...paths) {
34
51
  let result = "";
35
- for (let i = 0; i < paths.length; i++) {
36
- const path = paths[i];
37
- if (!result || result === "/") {
38
- result = path || "/";
52
+ for (const path of paths) {
53
+ if (!path) continue;
54
+ if (!result) {
55
+ result = path;
39
56
  continue;
40
57
  }
41
- if (!path || path === "/") continue;
58
+ if (path === "/") continue;
42
59
  const resultHasTrailing = result[result.length - 1] === "/";
43
60
  const pathHasLeading = path[0] === "/";
44
61
  if (resultHasTrailing && pathHasLeading) result += path.slice(1);
@@ -53,7 +70,7 @@ function joinURL(...paths) {
53
70
  function withBase(input = "", base = "") {
54
71
  if (!base || base === "/") return input;
55
72
  const _base = withoutTrailingSlash(base);
56
- if (input.startsWith(_base)) return input;
73
+ if (input.startsWith(_base) && (input.length === _base.length || input[_base.length] === "/" || input[_base.length] === "?")) return input;
57
74
  return joinURL(_base, input);
58
75
  }
59
76
  /**
@@ -62,15 +79,21 @@ function withBase(input = "", base = "") {
62
79
  function withoutBase(input = "", base = "") {
63
80
  if (!base || base === "/") return input;
64
81
  const _base = withoutTrailingSlash(base);
65
- if (!input.startsWith(_base)) return input;
82
+ if (!input.startsWith(_base) || input.length !== _base.length && input[_base.length] !== "/" && input[_base.length] !== "?") return input;
66
83
  const trimmed = input.slice(_base.length);
67
84
  return trimmed[0] === "/" ? trimmed : `/${trimmed}`;
68
85
  }
69
86
  /**
70
- * Returns the pathname of the given path, which is the path without the query string.
87
+ * Returns the pathname of the given path, which is the path without the query string or hash.
71
88
  */
72
89
  function getPathname(path = "/") {
73
- return path.startsWith("/") ? path.split("?")[0] : new URL(path, "http://localhost").pathname;
90
+ if (!path.startsWith("/")) return new URL(path, "http://localhost").pathname;
91
+ let pathEnd = path.length;
92
+ const queryIndex = path.indexOf("?");
93
+ const hashIndex = path.indexOf("#");
94
+ if (queryIndex !== -1) pathEnd = queryIndex;
95
+ if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
96
+ return path.slice(0, pathEnd) || "/";
74
97
  }
75
98
  /**
76
99
  * Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
@@ -80,7 +103,7 @@ function withQuery(input, query) {
80
103
  const searchIndex = input.indexOf("?");
81
104
  const hasExistingParams = searchIndex !== -1;
82
105
  const base = hasExistingParams ? input.slice(0, searchIndex) : input;
83
- const searchParams = hasExistingParams ? new URLSearchParams(input.slice(searchIndex + 1)) : new URLSearchParams();
106
+ const searchParams = new URLSearchParams(hasExistingParams ? input.slice(searchIndex + 1) : void 0);
84
107
  for (const [key, value] of Object.entries(query)) {
85
108
  if (value === void 0) {
86
109
  searchParams.delete(key);
package/dist/result.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/result.d.ts
2
- type Result<T, E> = Ok<T> | Err<E>;
2
+ type Result<T, E> = Ok<T, E> | Err<T, E>;
3
3
  interface OkData<T> {
4
4
  value: T;
5
5
  error: undefined;
@@ -9,25 +9,67 @@ interface ErrData<E> {
9
9
  error: E;
10
10
  }
11
11
  type ResultData<T, E> = OkData<T> | ErrData<E>;
12
- declare class Ok<T> {
12
+ /**
13
+ * Successful result variant.
14
+ * @template T Success value type.
15
+ * @template E Error type (phantom - for type unification).
16
+ */
17
+ declare class Ok<T, E = never> {
13
18
  readonly value: T;
14
19
  readonly ok: true;
15
20
  constructor(value: T);
21
+ /** Transforms the success value. */
22
+ map<U>(fn: (value: T) => U): Ok<U, E>;
23
+ /** No-op on Ok, returns self with new error type. */
24
+ mapError<E2>(_fn: (error: E) => E2): Ok<T, E2>;
25
+ /** Chains a Result-returning function. */
26
+ andThen<U, E2>(fn: (value: T) => Result<U, E2>): Result<U, E | E2>;
27
+ /** Extracts the value. */
28
+ unwrap(_message?: string): T;
29
+ /** Returns the value, ignoring the fallback. */
30
+ unwrapOr<U>(_fallback: U): T;
31
+ /** Pattern matches on the Result. */
32
+ match<R>(handlers: {
33
+ ok: (value: T) => R;
34
+ err: (error: E) => R;
35
+ }): R;
16
36
  }
17
- declare class Err<E> {
37
+ /**
38
+ * Error result variant.
39
+ * @template T Success type (phantom - for type unification).
40
+ * @template E Error value type.
41
+ */
42
+ declare class Err<T, E> {
18
43
  readonly error: E;
19
44
  readonly ok: false;
20
45
  constructor(error: E);
46
+ /** No-op on Err, returns self with new value type. */
47
+ map<U>(_fn: (value: T) => U): Err<U, E>;
48
+ /** Transforms the error value. */
49
+ mapError<E2>(fn: (error: E) => E2): Err<T, E2>;
50
+ /** No-op on Err, returns self with widened error type. */
51
+ andThen<U, E2>(_fn: (value: T) => Result<U, E2>): Err<U, E | E2>;
52
+ /** Throws an error with the given message. */
53
+ unwrap(message?: string): never;
54
+ /** Returns the fallback value. */
55
+ unwrapOr<U>(fallback: U): T | U;
56
+ /** Pattern matches on the Result. */
57
+ match<R>(handlers: {
58
+ ok: (value: T) => R;
59
+ err: (error: E) => R;
60
+ }): R;
21
61
  }
22
- declare function ok<T>(value: T): Ok<T>;
23
- declare function err<E extends string = string>(error: E): Err<E>;
24
- declare function err<E = unknown>(error: E): Err<E>;
62
+ declare function ok<T, E = never>(value: T): Ok<T, E>;
63
+ declare function err<T = never, E extends string = string>(error: E): Err<T, E>;
64
+ declare function err<T = never, E = unknown>(error: E): Err<T, E>;
65
+ declare function isOk<T, E>(result: Result<T, E>): result is Ok<T, E>;
66
+ declare function isErr<T, E>(result: Result<T, E>): result is Err<T, E>;
25
67
  declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>;
26
68
  declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>;
27
- declare function unwrapResult<T>(result: Ok<T>): OkData<T>;
28
- declare function unwrapResult<E>(result: Err<E>): ErrData<E>;
69
+ declare function unwrapResult<T, E>(result: Ok<T, E>): OkData<T>;
70
+ declare function unwrapResult<T, E>(result: Err<T, E>): ErrData<E>;
29
71
  declare function unwrapResult<T, E>(result: Result<T, E>): ResultData<T, E>;
30
72
  declare function tryCatch<T, E = unknown>(fn: () => T): ResultData<T, E>;
31
73
  declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<ResultData<T, E>>;
32
74
  //#endregion
33
- export { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult };
75
+ export { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult };
package/dist/result.mjs CHANGED
@@ -1,17 +1,75 @@
1
1
  //#region src/result.ts
2
- var Ok = class {
2
+ /**
3
+ * Successful result variant.
4
+ * @template T Success value type.
5
+ * @template E Error type (phantom - for type unification).
6
+ */
7
+ var Ok = class Ok {
3
8
  value;
4
9
  ok = true;
5
10
  constructor(value) {
6
11
  this.value = value;
7
12
  }
13
+ /** Transforms the success value. */
14
+ map(fn) {
15
+ return new Ok(fn(this.value));
16
+ }
17
+ /** No-op on Ok, returns self with new error type. */
18
+ mapError(_fn) {
19
+ return this;
20
+ }
21
+ /** Chains a Result-returning function. */
22
+ andThen(fn) {
23
+ return fn(this.value);
24
+ }
25
+ /** Extracts the value. */
26
+ unwrap(_message) {
27
+ return this.value;
28
+ }
29
+ /** Returns the value, ignoring the fallback. */
30
+ unwrapOr(_fallback) {
31
+ return this.value;
32
+ }
33
+ /** Pattern matches on the Result. */
34
+ match(handlers) {
35
+ return handlers.ok(this.value);
36
+ }
8
37
  };
9
- var Err = class {
38
+ /**
39
+ * Error result variant.
40
+ * @template T Success type (phantom - for type unification).
41
+ * @template E Error value type.
42
+ */
43
+ var Err = class Err {
10
44
  error;
11
45
  ok = false;
12
46
  constructor(error) {
13
47
  this.error = error;
14
48
  }
49
+ /** No-op on Err, returns self with new value type. */
50
+ map(_fn) {
51
+ return this;
52
+ }
53
+ /** Transforms the error value. */
54
+ mapError(fn) {
55
+ return new Err(fn(this.error));
56
+ }
57
+ /** No-op on Err, returns self with widened error type. */
58
+ andThen(_fn) {
59
+ return this;
60
+ }
61
+ /** Throws an error with the given message. */
62
+ unwrap(message) {
63
+ throw new Error(message ?? `Unwrap called on Err: ${String(this.error)}`);
64
+ }
65
+ /** Returns the fallback value. */
66
+ unwrapOr(fallback) {
67
+ return fallback;
68
+ }
69
+ /** Pattern matches on the Result. */
70
+ match(handlers) {
71
+ return handlers.err(this.error);
72
+ }
15
73
  };
16
74
  function ok(value) {
17
75
  return new Ok(value);
@@ -19,6 +77,12 @@ function ok(value) {
19
77
  function err(error) {
20
78
  return new Err(error);
21
79
  }
80
+ function isOk(result) {
81
+ return result.ok === true;
82
+ }
83
+ function isErr(result) {
84
+ return result.ok === false;
85
+ }
22
86
  function toResult(fnOrPromise) {
23
87
  if (fnOrPromise instanceof Promise) return fnOrPromise.then(ok).catch(err);
24
88
  try {
@@ -42,4 +106,4 @@ function tryCatch(fnOrPromise) {
42
106
  }
43
107
 
44
108
  //#endregion
45
- export { Err, Ok, err, ok, toResult, tryCatch, unwrapResult };
109
+ export { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "utilful",
3
3
  "type": "module",
4
- "version": "2.3.0",
5
- "packageManager": "pnpm@10.21.0",
4
+ "version": "2.5.0",
5
+ "packageManager": "pnpm@10.28.0",
6
6
  "description": "A collection of TypeScript utilities",
7
7
  "author": "Johann Schopplich <hello@johannschopplich.com>",
8
8
  "license": "MIT",
@@ -78,13 +78,13 @@
78
78
  "release": "bumpp"
79
79
  },
80
80
  "devDependencies": {
81
- "@antfu/eslint-config": "^6.2.0",
82
- "@types/node": "^24.10.0",
83
- "bumpp": "^10.3.1",
84
- "eslint": "^9.39.1",
85
- "tsdown": "^0.16.1",
81
+ "@antfu/eslint-config": "^7.0.1",
82
+ "@types/node": "^25.0.9",
83
+ "bumpp": "^10.4.0",
84
+ "eslint": "^9.39.2",
85
+ "tsdown": "^0.19.0",
86
86
  "typescript": "^5.9.3",
87
- "vitest": "^4.0.8"
87
+ "vitest": "^4.0.17"
88
88
  },
89
89
  "pnpm": {
90
90
  "onlyBuiltDependencies": [