utilful 2.2.3 → 2.3.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
@@ -50,26 +50,24 @@ declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[]
50
50
 
51
51
  #### `createCSV`
52
52
 
53
- Converts an array of objects to a comma-separated values (CSV) string that contains only the `columns` specified.
53
+ Converts an array of objects to a comma-separated values (CSV) string. You can either specify which columns to include explicitly, or let the function automatically infer all columns from your data.
54
54
 
55
55
  ```ts
56
+ // With explicit columns
56
57
  declare function createCSV<T extends Record<string, unknown>>(
57
- data: T[],
58
+ data: readonly T[],
58
59
  columns: readonly (keyof T)[],
59
- options?: {
60
- /** @default ',' */
61
- delimiter?: string
62
- /** @default true */
63
- addHeader?: boolean
64
- /** @default false */
65
- quoteAll?: boolean
66
- /** @default '\n' */
67
- lineEnding?: string
68
- }
60
+ options?: CSVCreateOptions
61
+ ): string
62
+
63
+ // With automatic column inference
64
+ declare function createCSV<T extends Record<string, unknown>>(
65
+ data: readonly T[],
66
+ options?: CSVCreateOptions
69
67
  ): string
70
68
  ```
71
69
 
72
- **Example:**
70
+ **Example with explicit columns:**
73
71
 
74
72
  ```ts
75
73
  const data = [
@@ -77,12 +75,32 @@ const data = [
77
75
  { name: 'Jane', age: '25', city: 'Boston' }
78
76
  ]
79
77
 
78
+ // Only include 'name' and 'age' columns
80
79
  const csv = createCSV(data, ['name', 'age'])
81
80
  // name,age
82
81
  // John,30
83
82
  // Jane,25
84
83
  ```
85
84
 
85
+ **Example with automatic column inference:**
86
+
87
+ When you omit the `columns` parameter, `createCSV` automatically collects all unique keys from your data in first-seen order. This is particularly useful when working with data that has varying structures:
88
+
89
+ ```ts
90
+ const rows = [
91
+ { name: 'John', age: '30' },
92
+ { name: 'Jane', city: 'Boston' },
93
+ { name: 'Bob', age: '40', city: 'Chicago' }
94
+ ]
95
+
96
+ // All columns are automatically detected: name, age, city
97
+ const csv = createCSV(rows)
98
+ // name,age,city
99
+ // John,30,
100
+ // Jane,,Boston
101
+ // Bob,40,Chicago
102
+ ```
103
+
86
104
  #### `parseCSV`
87
105
 
88
106
  Parses a comma-separated values (CSV) string into an array of objects.
@@ -119,7 +137,7 @@ const csv = `
119
137
  name,age
120
138
  John,30
121
139
  Jane,25
122
- `
140
+ `.trim()
123
141
 
124
142
  const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
125
143
  ```
@@ -134,7 +152,7 @@ Recursively assigns missing properties from defaults to the source object. The s
134
152
 
135
153
  **Key Features:**
136
154
 
137
- - **Null/undefined handling**: `null` and `undefined` values in source are replaced with defaults
155
+ - **Nullish handling**: `null` and `undefined` values in source are replaced with defaults
138
156
  - **Array concatenation**: Arrays are concatenated (source + defaults)
139
157
  - **Deep merging**: Nested objects are recursively merged
140
158
  - **Type safety**: Preserves TypeScript types
@@ -171,7 +189,7 @@ const result = defu(
171
189
  // Result: { items: ['a', 'b', 'c', 'd'] }
172
190
  ```
173
191
 
174
- **Null/undefined handling:**
192
+ **Handling null/undefined:**
175
193
 
176
194
  ```ts
177
195
  const result = defu(
@@ -455,8 +473,6 @@ else
455
473
 
456
474
  The `Result` type represents either success (`Ok`) or failure (`Err`).
457
475
 
458
- **Type Definition:**
459
-
460
476
  ```ts
461
477
  type Result<T, E> = Ok<T> | Err<E>
462
478
  ```
@@ -485,8 +501,6 @@ const result = new Err('Something went wrong')
485
501
 
486
502
  Shorthand function to create an `Ok` result. Use it to wrap a successful value.
487
503
 
488
- **Type Definition:**
489
-
490
504
  ```ts
491
505
  declare function ok<T>(value: T): Ok<T>
492
506
  ```
@@ -495,8 +509,6 @@ declare function ok<T>(value: T): Ok<T>
495
509
 
496
510
  Shorthand function to create an `Err` result. Use it to wrap an error value.
497
511
 
498
- **Type Definition:**
499
-
500
512
  ```ts
501
513
  declare function err<E extends string = string>(err: E): Err<E>
502
514
  declare function err<E = unknown>(err: E): Err<E>
@@ -506,8 +518,6 @@ declare function err<E = unknown>(err: E): Err<E>
506
518
 
507
519
  Wraps a function that might throw an error and returns a `Result` with the result of the function.
508
520
 
509
- **Type Definition:**
510
-
511
521
  ```ts
512
522
  declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
513
523
  declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
@@ -517,25 +527,28 @@ declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T
517
527
 
518
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.
519
529
 
520
- **Example:**
521
-
522
530
  ```ts
523
- const result = toResult(() => JSON.parse('{"foo":"bar"}'))
524
- const { value, error } = unwrapResult(result)
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 }
525
534
  ```
526
535
 
527
- **Type Definition:**
536
+ **Example:**
528
537
 
529
538
  ```ts
530
- declare function unwrapResult<T>(result: Ok<T>): { value: T, error: undefined }
531
- declare function unwrapResult<E>(result: Err<E>): { value: undefined, error: E }
532
- declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
539
+ const result = toResult(() => JSON.parse('{"foo":"bar"}'))
540
+ const { value, error } = unwrapResult(result)
533
541
  ```
534
542
 
535
543
  #### `tryCatch`
536
544
 
537
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.
538
546
 
547
+ ```ts
548
+ declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
549
+ declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
550
+ ```
551
+
539
552
  **Example:**
540
553
 
541
554
  ```ts
@@ -548,13 +561,6 @@ const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
548
561
  const { value, error } = await tryCatch(fetch('https://api.example.com/data').then(r => r.json()))
549
562
  ```
550
563
 
551
- **Type Definition:**
552
-
553
- ```ts
554
- declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
555
- declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
556
- ```
557
-
558
564
  ### String
559
565
 
560
566
  #### `template`
package/dist/csv.d.mts CHANGED
@@ -4,10 +4,32 @@
4
4
  */
5
5
  type CSVRow<T extends string = string> = Record<T, string>;
6
6
  /**
7
- * Converts an array of objects to a comma-separated values (CSV) string
8
- * that contains only the `columns` specified.
7
+ * Options for the `createCSV` function.
8
+ */
9
+ interface CSVCreateOptions {
10
+ /** @default ',' */
11
+ delimiter?: string;
12
+ /** @default true */
13
+ addHeader?: boolean;
14
+ /** @default false */
15
+ quoteAll?: boolean;
16
+ /** @default '\n' */
17
+ lineEnding?: string;
18
+ }
19
+ /**
20
+ * Converts an array of objects to a comma-separated values (CSV) string.
21
+ *
22
+ * @remarks
23
+ * When `columns` is omitted, the function automatically infers columns by collecting
24
+ * the union of all keys across all objects in first-seen order. This means if different
25
+ * objects have different keys, all keys will be included in the CSV. Objects missing
26
+ * certain keys will have empty values for those columns.
27
+ *
28
+ * When `columns` is provided explicitly, only those columns are included in the output,
29
+ * allowing you to control column order and filter out unwanted properties.
9
30
  *
10
31
  * @example
32
+ * // With explicit columns
11
33
  * const data = [
12
34
  * { name: 'John', age: '30', city: 'New York' },
13
35
  * { name: 'Jane', age: '25', city: 'Boston' }
@@ -17,17 +39,21 @@ type CSVRow<T extends string = string> = Record<T, string>;
17
39
  * // name,age
18
40
  * // John,30
19
41
  * // Jane,25
42
+ *
43
+ * @example
44
+ * // With inferred columns (union of all keys in first-seen order)
45
+ * const rows = [
46
+ * { name: 'John', age: '30' },
47
+ * { name: 'Jane', city: 'Boston' },
48
+ * ]
49
+ *
50
+ * const csv = createCSV(rows)
51
+ * // name,age,city
52
+ * // John,30,
53
+ * // Jane,,Boston
20
54
  */
21
- declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: readonly (keyof T)[], options?: {
22
- /** @default ',' */
23
- delimiter?: string;
24
- /** @default true */
25
- addHeader?: boolean;
26
- /** @default false */
27
- quoteAll?: boolean;
28
- /** @default '\n' */
29
- lineEnding?: string;
30
- }): string;
55
+ declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], columns: readonly (keyof T)[], options?: CSVCreateOptions): string;
56
+ declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], options?: CSVCreateOptions): string;
31
57
  /**
32
58
  * Escapes a value for a CSV string.
33
59
  *
@@ -75,4 +101,4 @@ declare function parseCSV<Header extends string>(csv?: string | null | undefined
75
101
  strict?: boolean;
76
102
  }): CSVRow<Header>[];
77
103
  //#endregion
78
- export { CSVRow, createCSV, escapeCSVValue, parseCSV };
104
+ export { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV };
package/dist/csv.mjs CHANGED
@@ -1,20 +1,15 @@
1
1
  //#region src/csv.ts
2
- /**
3
- * Converts an array of objects to a comma-separated values (CSV) string
4
- * that contains only the `columns` specified.
5
- *
6
- * @example
7
- * const data = [
8
- * { name: 'John', age: '30', city: 'New York' },
9
- * { name: 'Jane', age: '25', city: 'Boston' }
10
- * ]
11
- *
12
- * const csv = createCSV(data, ['name', 'age'])
13
- * // name,age
14
- * // John,30
15
- * // Jane,25
16
- */
17
- function createCSV(data, columns, options = {}) {
2
+ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
3
+ let columns;
4
+ let options;
5
+ if (Array.isArray(columnsOrOptions)) {
6
+ columns = columnsOrOptions;
7
+ options = maybeOptions;
8
+ } else {
9
+ columns = inferColumns(data);
10
+ options = columnsOrOptions ?? {};
11
+ }
12
+ if (columns.length === 0 && data.length === 0) return "";
18
13
  const { delimiter = ",", addHeader = true, quoteAll = false, lineEnding = "\n" } = options;
19
14
  if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
20
15
  const formatCell = (value) => escapeCSVValue(value, {
@@ -22,7 +17,11 @@ function createCSV(data, columns, options = {}) {
22
17
  quoteAll
23
18
  });
24
19
  const rows = data.map((obj) => columns.map((key) => formatCell(obj[key])).join(delimiter));
25
- if (addHeader) return columns.map(formatCell).join(delimiter) + lineEnding + rows.join(lineEnding);
20
+ if (addHeader) {
21
+ const header = columns.map(formatCell).join(delimiter);
22
+ if (rows.length === 0) return header;
23
+ return header + lineEnding + rows.join(lineEnding);
24
+ }
26
25
  return rows.join(lineEnding);
27
26
  }
28
27
  /**
@@ -122,6 +121,20 @@ function parseCSV(csv, options = {}) {
122
121
  }));
123
122
  });
124
123
  }
124
+ /**
125
+ * Infers column names from data by collecting the union of keys
126
+ * across all rows in first-seen order.
127
+ */
128
+ function inferColumns(rows) {
129
+ const seenColumns = /* @__PURE__ */ new Set();
130
+ const columns = [];
131
+ for (const row of rows) if (row && typeof row === "object") for (const columnName of Object.keys(row)) {
132
+ if (seenColumns.has(columnName)) continue;
133
+ seenColumns.add(columnName);
134
+ columns.push(columnName);
135
+ }
136
+ return columns;
137
+ }
125
138
 
126
139
  //#endregion
127
140
  export { createCSV, escapeCSVValue, parseCSV };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MaybeArray, toArray } from "./array.mjs";
2
- import { CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
2
+ import { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV } 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";
@@ -9,4 +9,4 @@ import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSla
9
9
  import { Err, ErrData, Ok, OkData, Result, ResultData, err, 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, 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, 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 };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export { };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "utilful",
3
3
  "type": "module",
4
- "version": "2.2.3",
4
+ "version": "2.3.0",
5
5
  "packageManager": "pnpm@10.21.0",
6
6
  "description": "A collection of TypeScript utilities",
7
7
  "author": "Johann Schopplich <hello@johannschopplich.com>",