utilful 3.0.1 → 3.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/README.md CHANGED
@@ -21,13 +21,20 @@ A collection of TypeScript utilities that I use across my projects.
21
21
 
22
22
  ```bash
23
23
  # npm
24
- npm install -D utilful
24
+ npm install utilful
25
25
 
26
26
  # pnpm
27
- pnpm add -D utilful
27
+ pnpm add utilful
28
28
 
29
29
  # yarn
30
- yarn add -D utilful
30
+ yarn add utilful
31
+ ```
32
+
33
+ Every module is also available on its own subpath, so you can import just the part you need:
34
+
35
+ ```ts
36
+ import { defu } from 'utilful' // Everything
37
+ import { joinURL } from 'utilful/path' // Just the path helpers
31
38
  ```
32
39
 
33
40
  ## API
@@ -113,12 +120,12 @@ interface CSVParseOptions {
113
120
  /** @default ',' */
114
121
  delimiter?: string
115
122
  /**
116
- * Trim whitespace from unquoted headers and values.
123
+ * Whether to trim whitespace from unquoted headers and values.
117
124
  * @default false
118
125
  */
119
126
  trim?: boolean
120
127
  /**
121
- * Throw if a row's field count does not match the header row.
128
+ * Whether to throw if a row's field count does not match the header row.
122
129
  * @default true
123
130
  */
124
131
  strict?: boolean
@@ -130,7 +137,12 @@ declare function parseCSV<Header extends string>(
130
137
  ): CSVRow<Header>[]
131
138
  ```
132
139
 
133
- The parser accepts a few lenient deviations from RFC 4180: LF, CR, and CRLF line endings are all recognized, whitespace between a closing quote and the next delimiter or line break is ignored, and quotes inside unquoted fields are kept as literal characters (a field only counts as quoted if it starts with a quote).
140
+ The parser accepts a few lenient deviations from RFC 4180:
141
+
142
+ - LF, CR, and CRLF line endings are all recognized
143
+ - whitespace between a closing quote and the next delimiter or line break is ignored
144
+ - quotes inside unquoted fields are kept as literal characters, since a field only counts as quoted if it starts with a quote
145
+ - text following a closing quote is appended to the field rather than rejected, so `"ab" cd` parses as `abcd`
134
146
 
135
147
  **Example:**
136
148
 
@@ -148,6 +160,9 @@ const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { na
148
160
 
149
161
  Creates a CSV stream from an iterable or async iterable of objects. Yields complete lines (header and/or data rows) including line endings – useful for large datasets that should not be buffered in memory.
150
162
 
163
+ > [!NOTE]
164
+ > Unlike `createCSV`, `columns` is required here. Inferring them would mean reading every row before writing the first one, which is exactly what streaming avoids.
165
+
151
166
  ```ts
152
167
  declare function createCSVStream<T extends Record<string, unknown>>(
153
168
  data: AsyncIterable<T> | Iterable<T>,
@@ -228,7 +243,7 @@ escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
228
243
 
229
244
  ### Defu
230
245
 
231
- Recursively assign default properties. Simplified version based on [unjs/defu](https://github.com/unjs/defu).
246
+ Fills in missing properties from a chain of defaults. A trimmed-down take on [unjs/defu](https://github.com/unjs/defu).
232
247
 
233
248
  #### `defu`
234
249
 
@@ -286,7 +301,7 @@ const result = defu(
286
301
 
287
302
  #### `createDefu`
288
303
 
289
- Creates a custom defu function with a custom merger.
304
+ Creates a `defu` variant that hands every property to your own merger first. Return `true` to signal you handled it; return nothing to fall back to the default behavior.
290
305
 
291
306
  ```ts
292
307
  type DefuMerger<T extends PlainObject = PlainObject> = (
@@ -325,8 +340,7 @@ Tiny functional event emitter / pubsub, based on [mitt](https://github.com/devel
325
340
  ```ts
326
341
  import { createEmitter } from 'utilful'
327
342
 
328
- // eslint-disable-next-line ts/consistent-type-definitions
329
- type Events = {
343
+ interface Events {
330
344
  foo: { a: string }
331
345
  }
332
346
 
@@ -388,12 +402,9 @@ async function loadModule() {
388
402
 
389
403
  #### `memoize`
390
404
 
391
- A simple general purpose memoizer utility.
405
+ Defers a computation until the value is first read, then caches it.
392
406
 
393
- - Lazily computes a value when accessed
394
- - Auto-caches the result by overwriting the getter
395
-
396
- Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
407
+ Useful for expensive setup you may never need. Unlike a plain getter, there is no runtime cost after the first read, because the getter replaces itself with the computed value.
397
408
 
398
409
  ```ts
399
410
  declare function memoize<T>(getter: () => T): { value: T }
@@ -426,15 +437,20 @@ declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof
426
437
 
427
438
  #### `deepApply`
428
439
 
429
- Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays (including arrays nested inside arrays).
440
+ Applies a callback to every key-value pair of the given object, and to every pair inside nested objects and arrays (including arrays nested inside arrays).
441
+
442
+ The callback also fires for nested objects, so `item` is whichever object the pair belongs to rather than the one you passed in.
430
443
 
431
444
  ```ts
432
- declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void
445
+ declare function deepApply<T extends Record<any, any>>(
446
+ data: T,
447
+ callback: (item: Record<string, any>, key: string, value: any) => void
448
+ ): void
433
449
  ```
434
450
 
435
451
  #### `isObject`
436
452
 
437
- Checks if a value is an object with the plain `[object Object]` tag. Returns `true` for object literals, class instances, and `null`-prototype objects.
453
+ Checks whether a value is an object. Object literals, class instances, and `null`-prototype objects all count; arrays, `Date`, `RegExp`, and `null` do not.
438
454
 
439
455
  ```ts
440
456
  declare function isObject(value: unknown): value is Record<any, any>
@@ -442,7 +458,7 @@ declare function isObject(value: unknown): value is Record<any, any>
442
458
 
443
459
  ### Path
444
460
 
445
- Utilities to build and normalize URL paths. All of them are also available from the `utilful/path` subpath export.
461
+ Utilities to build and normalize URL paths. They slice strings instead of parsing a full `URL`, which keeps them cheap enough for hot paths. Only `withQuery` and `getQuery` reach for `URLSearchParams`, where correct percent-encoding is worth the allocation.
446
462
 
447
463
  #### `withoutLeadingSlash` / `withLeadingSlash`
448
464
 
@@ -478,7 +494,7 @@ joinURL('/api/', '/users', '42') // '/api/users/42'
478
494
 
479
495
  #### `withBase` / `withoutBase`
480
496
 
481
- Adds or removes a base path – each is a no-op if the base is already present (or absent).
497
+ Adds or removes a base path – each is a no-op if the base is already present (or absent). An absolute URL is returned as it is, since no base path can prefix it.
482
498
 
483
499
  ```ts
484
500
  declare function withBase(input?: string, base?: string): string
@@ -489,20 +505,36 @@ declare function withoutBase(input?: string, base?: string): string
489
505
 
490
506
  ```ts
491
507
  withBase('/users', '/api') // '/api/users'
508
+ withBase('https://example.com/users', '/api') // 'https://example.com/users'
492
509
  withoutBase('/api/users', '/api') // '/users'
493
510
  ```
494
511
 
495
512
  #### `getPathname`
496
513
 
497
- Returns the pathname of the given path – everything before the query string or hash. Absolute URLs (with a scheme, e.g. `https://example.com/foo`) return the URL's pathname; all other inputs are returned unchanged with the query string and hash removed.
514
+ Returns the pathname of the given path – everything before the query string or hash. Absolute URLs return the part after the host, whether they carry a scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
515
+
516
+ The pathname is sliced out as written and never normalized, so percent-encoding and `..` segments survive:
498
517
 
499
518
  ```ts
500
519
  declare function getPathname(path?: string): string
501
520
  ```
502
521
 
522
+ ```ts
523
+ getPathname('/foo?bar#baz') // '/foo'
524
+ getPathname('https://example.com/foo') // '/foo'
525
+ getPathname('//example.com/foo') // '/foo'
526
+ getPathname('https://example.com') // '/'
527
+ getPathname('https://example.com/a/../b') // '/a/../b' – use `new URL` if you need this resolved
528
+ ```
529
+
503
530
  #### `withQuery`
504
531
 
505
- Returns the URL with the given query parameters merged in. `undefined` values remove the parameter, array values append one entry per item, and object values are JSON-stringified.
532
+ Returns the URL with the given query parameters merged in. A fragment stays where it belongs, at the very end.
533
+
534
+ - `undefined` removes the parameter
535
+ - `null` keeps the parameter with an empty value
536
+ - arrays append one entry per item, and empty arrays are skipped
537
+ - objects are JSON-stringified
506
538
 
507
539
  ```ts
508
540
  type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined
@@ -515,6 +547,25 @@ declare function withQuery(input: string, query?: QueryObject): string
515
547
 
516
548
  ```ts
517
549
  withQuery('/api/users', { page: 2, tags: ['a', 'b'] }) // '/api/users?page=2&tags=a&tags=b'
550
+ withQuery('/api/users#list', { page: 2 }) // '/api/users?page=2#list'
551
+ withQuery('/api/users?page=2', { page: undefined }) // '/api/users'
552
+ ```
553
+
554
+ #### `getQuery`
555
+
556
+ Reads the query parameters back out of a URL, ignoring the fragment. A parameter that appears more than once becomes an array of its values.
557
+
558
+ ```ts
559
+ type ParsedQuery = Record<string, string | string[]>
560
+
561
+ declare function getQuery(input: string): ParsedQuery
562
+ ```
563
+
564
+ **Example:**
565
+
566
+ ```ts
567
+ getQuery('/api/users?page=2&tags=a&tags=b') // { page: '2', tags: ['a', 'b'] }
568
+ getQuery('/api/users') // {}
518
569
  ```
519
570
 
520
571
  ### Result
@@ -525,7 +576,7 @@ The `Result` type represents either success (`Ok`) or failure (`Err`). It provid
525
576
  type Result<T, E> = Ok<T, E> | Err<T, E>
526
577
  ```
527
578
 
528
- Both `Ok` and `Err` carry phantom types for proper type inference in unions.
579
+ Both variants carry the success *and* the error type, so the two stay in sync as you chain `map` and `mapError` calls.
529
580
 
530
581
  **Basic example:**
531
582
 
@@ -616,7 +667,7 @@ Chains a function that returns a `Result`. Useful for composing fallible operati
616
667
 
617
668
  ```ts
618
669
  ok(2).andThen(x => x > 0 ? ok(x) : err('negative')) // Ok(2)
619
- err('fail').andThen(x => ok(x * 2)) // Err('fail') - short-circuits
670
+ err('fail').andThen(x => ok(x * 2)) // Err('fail') – short-circuits
620
671
  ```
621
672
 
622
673
  #### `Result.unwrap`
@@ -629,6 +680,16 @@ err('fail').unwrap() // throws Error
629
680
  err('fail').unwrap('custom message') // throws Error('custom message')
630
681
  ```
631
682
 
683
+ #### `Result.unwrapErr`
684
+
685
+ Extracts the error, or throws if the result is `Ok`. The mirror image of `unwrap`.
686
+
687
+ ```ts
688
+ err('fail').unwrapErr() // 'fail'
689
+ ok(42).unwrapErr() // throws Error
690
+ ok(42).unwrapErr('custom message') // throws Error('custom message')
691
+ ```
692
+
632
693
  #### `Result.unwrapOr`
633
694
 
634
695
  Extracts the value or returns a fallback.
@@ -690,6 +751,9 @@ declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undef
690
751
  declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
691
752
  ```
692
753
 
754
+ > [!NOTE]
755
+ > Like `toResult`, the function overload must be synchronous, and passing a function that returns a promise throws a `TypeError` rather than returning it as an error. Pass the promise itself.
756
+
693
757
  **Example:**
694
758
 
695
759
  ```ts
@@ -704,7 +768,9 @@ const { value, error } = await tryCatch(fetch('https://api.example.com').then(r
704
768
 
705
769
  #### `template`
706
770
 
707
- Simple template engine to replace variables in a string.
771
+ Replaces `{name}` placeholders in a string with the matching variable.
772
+
773
+ A placeholder with no matching variable is left as its own key, unless you pass a `fallback` – either a fixed string or a function receiving the key. A variable that is present but `null` or `undefined` is treated the same as a missing one. Only own properties are read, so `{constructor}` cannot reach prototype members.
708
774
 
709
775
  ```ts
710
776
  declare function template(
@@ -727,7 +793,10 @@ console.log(template(str, variables)) // Hello, world!
727
793
 
728
794
  #### `generateRandomId`
729
795
 
730
- Generates a random string. The function is ported from [`nanoid`](https://github.com/ai/nanoid). You can specify the size of the string and the dictionary of characters to use.
796
+ Generates a random string. Ported from [`nanoid`](https://github.com/ai/nanoid). You can specify the length and the dictionary of characters to draw from.
797
+
798
+ > [!WARNING]
799
+ > Backed by `Math.random()` and therefore not cryptographically secure. Use `crypto.randomUUID()` or `crypto.getRandomValues()` for session tokens, password resets, and anything else an attacker would like to guess.
731
800
 
732
801
  ```ts
733
802
  declare function generateRandomId(size?: number, dict?: string): string
package/dist/array.d.mts CHANGED
@@ -1,11 +1,5 @@
1
1
  //#region src/array.d.ts
2
- /**
3
- * Represents a value that can be either a single value or an array of values.
4
- */
5
2
  type MaybeArray<T> = T | T[];
6
- /**
7
- * Converts `MaybeArray<T>` to `Array<T>`.
8
- */
9
3
  declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[];
10
4
  //#endregion
11
5
  export { MaybeArray, toArray };
package/dist/array.mjs CHANGED
@@ -1,7 +1,4 @@
1
1
  //#region src/array.ts
2
- /**
3
- * Converts `MaybeArray<T>` to `Array<T>`.
4
- */
5
2
  function toArray(array) {
6
3
  array ??= [];
7
4
  return Array.isArray(array) ? array : [array];
package/dist/csv.d.mts CHANGED
@@ -1,11 +1,5 @@
1
1
  //#region src/csv.d.ts
2
- /**
3
- * Represents a row in a CSV file with column names of type T.
4
- */
5
2
  type CSVRow<T extends string = string> = Record<T, string>;
6
- /**
7
- * Options for the CSV creation functions.
8
- */
9
3
  interface CSVCreateOptions {
10
4
  /** @default ',' */
11
5
  delimiter?: string;
@@ -16,19 +10,16 @@ interface CSVCreateOptions {
16
10
  /** @default '\n' */
17
11
  lineEnding?: string;
18
12
  }
19
- /**
20
- * Options for the CSV parsing functions.
21
- */
22
13
  interface CSVParseOptions {
23
14
  /** @default ',' */
24
15
  delimiter?: string;
25
16
  /**
26
- * Trim whitespace from unquoted headers and values.
17
+ * Whether to trim whitespace from unquoted headers and values.
27
18
  * @default false
28
19
  */
29
20
  trim?: boolean;
30
21
  /**
31
- * Throw if a row's field count does not match the header row.
22
+ * Whether to throw if a row's field count does not match the header row.
32
23
  * @default true
33
24
  */
34
25
  strict?: boolean;
@@ -115,8 +106,8 @@ declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIt
115
106
  * Within quoted values, double quotes are escaped by doubling them.
116
107
  *
117
108
  * @example
118
- * escapeCSVValue('hello, world') // "hello, world"
119
- * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
109
+ * escapeCSVValue('hello, world') // '"hello, world"'
110
+ * escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
120
111
  */
121
112
  declare function escapeCSVValue(value: unknown, options?: {
122
113
  /** @default ',' */
@@ -132,10 +123,10 @@ declare function escapeCSVValue(value: unknown, options?: {
132
123
  * UTF-8 byte order mark is stripped.
133
124
  *
134
125
  * Parsing tolerances (lenient deviations from RFC 4180):
135
- * - LF, CR, and CRLF line endings are all accepted
136
- * - Whitespace between a closing quote and the next delimiter or line break is ignored
126
+ * - LF, CR, and CRLF line endings are all accepted.
127
+ * - Whitespace between a closing quote and the next delimiter or line break is ignored.
137
128
  * - A field is only treated as quoted if it starts with a quote; quotes inside
138
- * unquoted fields are kept as literal characters
129
+ * unquoted fields are kept as literal characters.
139
130
  *
140
131
  * @example
141
132
  * const csv = `name,age
package/dist/csv.mjs CHANGED
@@ -17,9 +17,9 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
17
17
  columns = inferColumns(data);
18
18
  options = columnsOrOptions ?? {};
19
19
  }
20
- if (columns.length === 0 && data.length === 0) return "";
21
20
  const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
22
21
  assertValidCSVDelimiter(delimiter);
22
+ if (columns.length === 0) return "";
23
23
  if (addHeader) {
24
24
  const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
25
25
  if (data.length === 0) return header;
@@ -97,8 +97,8 @@ function encodeCSVRow(row, columns, delimiter, quoteAll) {
97
97
  * Within quoted values, double quotes are escaped by doubling them.
98
98
  *
99
99
  * @example
100
- * escapeCSVValue('hello, world') // "hello, world"
101
- * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
100
+ * escapeCSVValue('hello, world') // '"hello, world"'
101
+ * escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
102
102
  */
103
103
  function escapeCSVValue(value, options = {}) {
104
104
  const { delimiter = COMMA, quoteAll = false } = options;
@@ -211,10 +211,10 @@ var CSVParserCore = class {
211
211
  * UTF-8 byte order mark is stripped.
212
212
  *
213
213
  * Parsing tolerances (lenient deviations from RFC 4180):
214
- * - LF, CR, and CRLF line endings are all accepted
215
- * - Whitespace between a closing quote and the next delimiter or line break is ignored
214
+ * - LF, CR, and CRLF line endings are all accepted.
215
+ * - Whitespace between a closing quote and the next delimiter or line break is ignored.
216
216
  * - A field is only treated as quoted if it starts with a quote; quotes inside
217
- * unquoted fields are kept as literal characters
217
+ * unquoted fields are kept as literal characters.
218
218
  *
219
219
  * @example
220
220
  * const csv = `name,age
package/dist/defu.d.mts CHANGED
@@ -1,19 +1,28 @@
1
1
  //#region src/defu.d.ts
2
2
  type PlainObject = Record<PropertyKey, any>;
3
3
  type DefuMerger<T extends PlainObject = PlainObject> = (target: T, key: PropertyKey, value: any, namespace: string) => boolean | void;
4
+ type Nullish = null | undefined | void;
5
+ /**
6
+ * Values that `isPlainObject` rejects at runtime, so the merged type keeps them
7
+ * intact rather than recursing into them. `PlainObject` cannot draw this line
8
+ * itself: its `any` value type makes every object type satisfy it, classes and
9
+ * built-ins included.
10
+ */
11
+ type NonPlainObject = ((...args: any[]) => any) | {
12
+ [Symbol.iterator]: any;
13
+ } | Date | RegExp | Promise<any> | Error | WeakMap<object, any> | WeakSet<object>;
4
14
  /**
5
15
  * Deeply merged result type of a source object over a list of defaults.
6
16
  */
7
17
  type Defu<Source, Defaults extends any[]> = Defaults extends [infer First, ...infer Rest] ? Defu<MergedObject<Source, First>, Rest> : Source;
8
- type MergedObject<Source, Defaults> = Source extends PlainObject ? Defaults extends PlainObject ? { [Key in keyof Source | keyof Defaults]: MergedValue<Key extends keyof Source ? Source[Key] : undefined, Key extends keyof Defaults ? Defaults[Key] : undefined>; } : Source : Source;
9
- type MergedValue<SourceValue, DefaultValue> = SourceValue extends null | undefined ? DefaultValue : SourceValue extends any[] ? DefaultValue extends any[] ? Array<SourceValue[number] | DefaultValue[number]> : SourceValue : SourceValue extends ((...args: any[]) => any) ? SourceValue : SourceValue extends PlainObject ? MergedObject<SourceValue, DefaultValue> : SourceValue;
10
18
  /**
11
- * Defu function type that accepts a source and multiple defaults
19
+ * Source merged over one defaults object, rebuilding only the shared keys.
20
+ * Everything else passes through `Omit`, which is what keeps optionality,
21
+ * `readonly` and the nominal identity of classes intact.
12
22
  */
23
+ type MergedObject<Source, Defaults> = Source extends PlainObject ? Defaults extends PlainObject ? Source extends Defaults ? Source : Omit<Source, keyof Source & keyof Defaults> & Omit<Defaults, keyof Source & keyof Defaults> & { -readonly [Key in keyof Source & keyof Defaults]: MergedValue<Source[Key], Defaults[Key]>; } : Source : Source;
24
+ type MergedValue<SourceValue, DefaultValue> = SourceValue extends Nullish ? DefaultValue : DefaultValue extends Nullish ? SourceValue : SourceValue extends readonly any[] ? DefaultValue extends readonly any[] ? Array<SourceValue[number] | DefaultValue[number]> : SourceValue : SourceValue extends NonPlainObject ? SourceValue : DefaultValue extends NonPlainObject ? SourceValue : MergedObject<SourceValue, DefaultValue>;
13
25
  type DefuFn = <Source extends PlainObject, Defaults extends PlainObject[]>(source: Source, ...defaults: Defaults) => Defu<Source, Defaults>;
14
- /**
15
- * Create a defu function with optional custom merger
16
- */
17
26
  declare function createDefu(merger?: DefuMerger): DefuFn;
18
27
  declare const defu: DefuFn;
19
28
  //#endregion
package/dist/defu.mjs CHANGED
@@ -1,7 +1,4 @@
1
1
  //#region src/defu.ts
2
- /**
3
- * Create a defu function with optional custom merger
4
- */
5
2
  function createDefu(merger) {
6
3
  return ((source, ...defaults) => {
7
4
  return defaults.reduce((mergedResult, currentDefaults) => _defu(mergedResult, currentDefaults ?? {}, "", merger), source ?? {});
@@ -4,8 +4,8 @@ type Handler<T = unknown> = (event: T) => void;
4
4
  type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
5
5
  type EventHandlerList<T = unknown> = Handler<T>[];
6
6
  type WildCardEventHandlerList<T = Record<string, unknown>> = WildcardHandler<T>[];
7
- type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
8
- interface Emitter<Events extends Record<EventType, unknown>> {
7
+ type EventHandlerMap<Events extends Record<EventType, any>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
8
+ interface Emitter<Events extends Record<EventType, any>> {
9
9
  events: EventHandlerMap<Events>;
10
10
  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
11
11
  on(type: "*", handler: WildcardHandler<Events>): void;
@@ -15,11 +15,11 @@ interface Emitter<Events extends Record<EventType, unknown>> {
15
15
  emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
16
16
  }
17
17
  /**
18
- * Simple functional event emitter / pubsub.
18
+ * Creates a functional pubsub event emitter.
19
19
  *
20
20
  * @remarks Ported from `mitt`.
21
21
  * @see https://github.com/developit/mitt
22
22
  */
23
- declare function createEmitter<Events extends Record<EventType, unknown>>(events?: EventHandlerMap<Events>): Emitter<Events>;
23
+ declare function createEmitter<Events extends Record<EventType, any>>(events?: EventHandlerMap<Events>): Emitter<Events>;
24
24
  //#endregion
25
25
  export { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter };
package/dist/emitter.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/emitter.ts
2
2
  /**
3
- * Simple functional event emitter / pubsub.
3
+ * Creates a functional pubsub event emitter.
4
4
  *
5
5
  * @remarks Ported from `mitt`.
6
6
  * @see https://github.com/developit/mitt
@@ -13,9 +13,7 @@ function createEmitter(events) {
13
13
  */
14
14
  events,
15
15
  /**
16
- * Register an event handler for the given type.
17
- *
18
- * @memberOf createEmitter
16
+ * Registers an event handler for the given type.
19
17
  */
20
18
  on(type, handler) {
21
19
  const handlers = events.get(type);
@@ -23,12 +21,10 @@ function createEmitter(events) {
23
21
  else events.set(type, [handler]);
24
22
  },
25
23
  /**
26
- * Remove an event handler for the given type.
24
+ * Removes an event handler for the given type.
27
25
  *
28
26
  * @remarks
29
27
  * If `handler` is omitted, all handlers of the given type are removed.
30
- *
31
- * @memberOf createEmitter
32
28
  */
33
29
  off(type, handler) {
34
30
  const handlers = events.get(type);
@@ -36,13 +32,11 @@ function createEmitter(events) {
36
32
  else events.set(type, []);
37
33
  },
38
34
  /**
39
- * Invoke all handlers for the given type.
35
+ * Invokes all handlers for the given type.
40
36
  *
41
37
  * @remarks
42
38
  * If present, `'*'` handlers are invoked after type-matched handlers.
43
- * Manually firing '*' handlers is not supported.
44
- *
45
- * @memberOf createEmitter
39
+ * Manually firing `'*'` handlers is not supported.
46
40
  */
47
41
  emit(type, evt) {
48
42
  let handlers = events.get(type);
package/dist/index.d.mts CHANGED
@@ -5,8 +5,8 @@ import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCar
5
5
  import { tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
8
- import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
8
+ import { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
9
  import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
10
  import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
11
- import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
12
- export { AutocompletableString, BrandedType, CSVCreateOptions, CSVParseOptions, CSVRow, Defu, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, TEMPLATE_PLACEHOLDER_RE, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
11
+ import { BrandedType, UnifyIntersection } from "./types.mjs";
12
+ export { BrandedType, CSVCreateOptions, CSVParseOptions, CSVRow, Defu, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, MaybeArray, Ok, OkData, ParsedQuery, QueryObject, QueryValue, Result, ResultData, TEMPLATE_PLACEHOLDER_RE, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { createEmitter } from "./emitter.mjs";
5
5
  import { tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
8
- import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
8
+ import { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
9
  import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
10
  import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
11
- export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
11
+ export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/json.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/json.d.ts
2
2
  /**
3
- * Type-safe wrapper around `JSON.parse`.
3
+ * Wraps `JSON.parse` with a typed return value.
4
4
  *
5
5
  * @remarks
6
6
  * Falls back to the original value if parsing fails or the value is not a string.
package/dist/json.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/json.ts
2
2
  /**
3
- * Type-safe wrapper around `JSON.parse`.
3
+ * Wraps `JSON.parse` with a typed return value.
4
4
  *
5
5
  * @remarks
6
6
  * Falls back to the original value if parsing fails or the value is not a string.
package/dist/module.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/module.d.ts
2
2
  /**
3
- * Interop helper for default exports.
3
+ * Unwraps a module's `default` export, or returns the module unchanged if it has none.
4
4
  *
5
5
  * @example
6
6
  * const mod = await interopDefault(import('./module.js'))
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/module.ts
2
2
  /**
3
- * Interop helper for default exports.
3
+ * Unwraps a module's `default` export, or returns the module unchanged if it has none.
4
4
  *
5
5
  * @example
6
6
  * const mod = await interopDefault(import('./module.js'))
package/dist/object.d.mts CHANGED
@@ -1,8 +1,6 @@
1
1
  //#region src/object.d.ts
2
2
  /**
3
- * A simple general purpose memoizer utility.
4
- * - Lazily computes a value when accessed
5
- * - Auto-caches the result by overwriting the getter
3
+ * Wraps a getter so its value is computed on first access and cached from then on.
6
4
  *
7
5
  * @remarks
8
6
  * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
@@ -17,17 +15,22 @@ declare function memoize<T>(getter: () => T): {
17
15
  value: T;
18
16
  };
19
17
  /**
20
- * Strictly typed `Object.keys`.
18
+ * Wraps `Object.keys` with a stricter return type.
21
19
  */
22
20
  declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${Extract<keyof T, string | number>}`>;
23
21
  /**
24
- * Strictly typed `Object.entries`.
22
+ * Wraps `Object.entries` with a stricter return type.
25
23
  */
26
24
  declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>;
27
25
  /**
28
- * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays (including arrays nested inside arrays).
26
+ * Applies a callback to every key-value pair of the given object, and to every pair
27
+ * inside nested objects and arrays (including arrays nested inside arrays).
28
+ *
29
+ * @remarks
30
+ * The callback also fires for nested objects, so `item` is whichever object the pair
31
+ * belongs to rather than the one that was passed in.
29
32
  */
30
- declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void;
33
+ declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: Record<string, any>, key: string, value: any) => void): void;
31
34
  /**
32
35
  * Checks if a value is an object with the plain `[object Object]` tag.
33
36
  *
package/dist/object.mjs CHANGED
@@ -1,8 +1,6 @@
1
1
  //#region src/object.ts
2
2
  /**
3
- * A simple general purpose memoizer utility.
4
- * - Lazily computes a value when accessed
5
- * - Auto-caches the result by overwriting the getter
3
+ * Wraps a getter so its value is computed on first access and cached from then on.
6
4
  *
7
5
  * @remarks
8
6
  * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
@@ -21,19 +19,24 @@ function memoize(getter) {
21
19
  } };
22
20
  }
23
21
  /**
24
- * Strictly typed `Object.keys`.
22
+ * Wraps `Object.keys` with a stricter return type.
25
23
  */
26
24
  function objectKeys(obj) {
27
25
  return Object.keys(obj);
28
26
  }
29
27
  /**
30
- * Strictly typed `Object.entries`.
28
+ * Wraps `Object.entries` with a stricter return type.
31
29
  */
32
30
  function objectEntries(obj) {
33
31
  return Object.entries(obj);
34
32
  }
35
33
  /**
36
- * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays (including arrays nested inside arrays).
34
+ * Applies a callback to every key-value pair of the given object, and to every pair
35
+ * inside nested objects and arrays (including arrays nested inside arrays).
36
+ *
37
+ * @remarks
38
+ * The callback also fires for nested objects, so `item` is whichever object the pair
39
+ * belongs to rather than the one that was passed in.
37
40
  */
38
41
  function deepApply(data, callback) {
39
42
  for (const [key, value] of Object.entries(data)) {
package/dist/path.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  //#region src/path.d.ts
2
2
  type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined;
3
3
  type QueryObject = Record<string, QueryValue | QueryValue[]>;
4
+ /** Query parameters as read back from a URL, where a repeated key holds all its values. */
5
+ type ParsedQuery = Record<string, string | string[]>;
4
6
  /**
5
7
  * Removes the leading slash from the given path if it has one.
6
8
  */
@@ -25,6 +27,11 @@ declare function withTrailingSlash(path?: string): string;
25
27
  declare function joinURL(...paths: (string | undefined)[]): string;
26
28
  /**
27
29
  * Adds the base path to the input path, if it is not already present.
30
+ *
31
+ * @remarks
32
+ * An absolute URL is returned as it is, since a base path cannot prefix one –
33
+ * whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
34
+ * (`//example.com/foo`).
28
35
  */
29
36
  declare function withBase(input?: string, base?: string): string;
30
37
  /**
@@ -35,13 +42,23 @@ declare function withoutBase(input?: string, base?: string): string;
35
42
  * Returns the pathname of the given path, which is the path without the query string or hash.
36
43
  *
37
44
  * @remarks
38
- * Absolute URLs (with a scheme, e.g. `https://example.com/foo`) return the URL's pathname.
39
- * All other inputs are returned unchanged with the query string and hash removed.
45
+ * Absolute URLs return the segment after the authority, whether they carry a
46
+ * scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
47
+ * The result is sliced out verbatim, never normalized, so percent-encoding and
48
+ * `..` segments survive as written.
40
49
  */
41
50
  declare function getPathname(path?: string): string;
42
51
  /**
43
- * Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
52
+ * Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
44
53
  */
45
54
  declare function withQuery(input: string, query?: QueryObject): string;
55
+ /**
56
+ * Reads the query parameters of the given URL, ignoring the fragment.
57
+ *
58
+ * @remarks
59
+ * A parameter that appears more than once becomes an array of its values,
60
+ * in the order they appear. Values are percent-decoded.
61
+ */
62
+ declare function getQuery(input: string): ParsedQuery;
46
63
  //#endregion
47
- export { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
64
+ export { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/path.mjs CHANGED
@@ -1,4 +1,14 @@
1
1
  //#region src/path.ts
2
+ const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
3
+ /**
4
+ * Returns the index at which the authority of an absolute URL starts, or `-1`
5
+ * for a path that carries none. A protocol-relative input such as
6
+ * `//example.com/foo` counts as absolute: its authority follows the leading `//`.
7
+ */
8
+ function getAuthorityStart(path) {
9
+ if (path.startsWith("//")) return 2;
10
+ return URL_SCHEME_RE.test(path) ? path.indexOf("://") + 3 : -1;
11
+ }
2
12
  /**
3
13
  * Removes the leading slash from the given path if it has one.
4
14
  */
@@ -66,9 +76,14 @@ function joinURL(...paths) {
66
76
  }
67
77
  /**
68
78
  * Adds the base path to the input path, if it is not already present.
79
+ *
80
+ * @remarks
81
+ * An absolute URL is returned as it is, since a base path cannot prefix one –
82
+ * whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
83
+ * (`//example.com/foo`).
69
84
  */
70
85
  function withBase(input = "", base = "") {
71
- if (!base || base === "/") return input;
86
+ if (!base || base === "/" || getAuthorityStart(input) !== -1) return input;
72
87
  const _base = withoutTrailingSlash(base);
73
88
  if (input.startsWith(_base) && (input.length === _base.length || input[_base.length] === "/" || input[_base.length] === "?" || input[_base.length] === "#")) return input;
74
89
  return joinURL(_base, input);
@@ -83,32 +98,47 @@ function withoutBase(input = "", base = "") {
83
98
  const trimmed = input.slice(_base.length);
84
99
  return trimmed[0] === "/" ? trimmed : `/${trimmed}`;
85
100
  }
86
- const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
87
101
  /**
88
102
  * Returns the pathname of the given path, which is the path without the query string or hash.
89
103
  *
90
104
  * @remarks
91
- * Absolute URLs (with a scheme, e.g. `https://example.com/foo`) return the URL's pathname.
92
- * All other inputs are returned unchanged with the query string and hash removed.
105
+ * Absolute URLs return the segment after the authority, whether they carry a
106
+ * scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
107
+ * The result is sliced out verbatim, never normalized, so percent-encoding and
108
+ * `..` segments survive as written.
93
109
  */
94
110
  function getPathname(path = "/") {
95
- if (URL_SCHEME_RE.test(path)) return new URL(path).pathname;
111
+ let pathStart = 0;
112
+ const authorityStart = getAuthorityStart(path);
113
+ if (authorityStart !== -1) {
114
+ let authorityEnd = path.length;
115
+ for (let i = authorityStart; i < path.length; i++) {
116
+ const character = path[i];
117
+ if (character === "/" || character === "?" || character === "#") {
118
+ authorityEnd = i;
119
+ break;
120
+ }
121
+ }
122
+ if (path[authorityEnd] !== "/") return "/";
123
+ pathStart = authorityEnd;
124
+ }
96
125
  let pathEnd = path.length;
97
- const queryIndex = path.indexOf("?");
98
- const hashIndex = path.indexOf("#");
126
+ const queryIndex = path.indexOf("?", pathStart);
127
+ const hashIndex = path.indexOf("#", pathStart);
99
128
  if (queryIndex !== -1) pathEnd = queryIndex;
100
129
  if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
101
- return path.slice(0, pathEnd) || "/";
130
+ return path.slice(pathStart, pathEnd) || "/";
102
131
  }
103
132
  /**
104
- * Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
133
+ * Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
105
134
  */
106
135
  function withQuery(input, query) {
107
136
  if (!query || Object.keys(query).length === 0) return input;
108
- const searchIndex = input.indexOf("?");
137
+ const { beforeHash, hash } = splitFragment(input);
138
+ const searchIndex = beforeHash.indexOf("?");
109
139
  const hasExistingParams = searchIndex !== -1;
110
- const base = hasExistingParams ? input.slice(0, searchIndex) : input;
111
- const searchParams = new URLSearchParams(hasExistingParams ? input.slice(searchIndex + 1) : void 0);
140
+ const base = hasExistingParams ? beforeHash.slice(0, searchIndex) : beforeHash;
141
+ const searchParams = new URLSearchParams(hasExistingParams ? beforeHash.slice(searchIndex + 1) : void 0);
112
142
  for (const [key, value] of Object.entries(query)) {
113
143
  if (value === void 0) {
114
144
  searchParams.delete(key);
@@ -120,7 +150,37 @@ function withQuery(input, query) {
120
150
  } else searchParams.set(key, normalizeQueryValue(value));
121
151
  }
122
152
  const queryString = searchParams.toString();
123
- return queryString ? `${base}?${queryString}` : base;
153
+ return queryString ? `${base}?${queryString}${hash}` : base + hash;
154
+ }
155
+ /**
156
+ * Reads the query parameters of the given URL, ignoring the fragment.
157
+ *
158
+ * @remarks
159
+ * A parameter that appears more than once becomes an array of its values,
160
+ * in the order they appear. Values are percent-decoded.
161
+ */
162
+ function getQuery(input) {
163
+ const { beforeHash } = splitFragment(input);
164
+ const searchIndex = beforeHash.indexOf("?");
165
+ if (searchIndex === -1) return {};
166
+ const query = Object.create(null);
167
+ for (const [key, value] of new URLSearchParams(beforeHash.slice(searchIndex + 1))) {
168
+ const existingValue = query[key];
169
+ if (existingValue === void 0) query[key] = value;
170
+ else if (Array.isArray(existingValue)) existingValue.push(value);
171
+ else query[key] = [existingValue, value];
172
+ }
173
+ return { ...query };
174
+ }
175
+ function splitFragment(input) {
176
+ const hashIndex = input.indexOf("#");
177
+ return hashIndex === -1 ? {
178
+ beforeHash: input,
179
+ hash: ""
180
+ } : {
181
+ beforeHash: input.slice(0, hashIndex),
182
+ hash: input.slice(hashIndex)
183
+ };
124
184
  }
125
185
  function normalizeQueryValue(value) {
126
186
  if (value === null) return "";
@@ -129,4 +189,4 @@ function normalizeQueryValue(value) {
129
189
  return String(value);
130
190
  }
131
191
  //#endregion
132
- export { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
192
+ export { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/result.d.mts CHANGED
@@ -11,8 +11,8 @@ interface ErrData<E> {
11
11
  type ResultData<T, E> = OkData<T> | ErrData<E>;
12
12
  /**
13
13
  * Successful result variant.
14
- * @template T Success value type.
15
- * @template E Error type (phantom - for type unification).
14
+ * @template T Success value type
15
+ * @template E Error type (phantom – for type unification)
16
16
  */
17
17
  declare class Ok<T, E = never> {
18
18
  readonly value: T;
@@ -20,15 +20,14 @@ declare class Ok<T, E = never> {
20
20
  constructor(value: T);
21
21
  /** Transforms the success value. */
22
22
  map<U>(fn: (value: T) => U): Ok<U, E>;
23
- /** No-op on Ok, returns self with new error type. */
23
+ /** Returns this `Ok` unchanged, with a new error type. */
24
24
  mapError<E2>(_fn: (error: E) => E2): Ok<T, E2>;
25
- /** Chains a Result-returning function. */
25
+ /** Chains a `Result`-returning function. */
26
26
  andThen<U, E2>(fn: (value: T) => Result<U, E2>): Result<U, E | E2>;
27
- /** Extracts the value. */
28
27
  unwrap(_message?: string): T;
28
+ unwrapErr(message?: string): never;
29
29
  /** Returns the value, ignoring the fallback. */
30
30
  unwrapOr<U>(_fallback: U): T;
31
- /** Pattern matches on the Result. */
32
31
  match<R>(handlers: {
33
32
  ok: (value: T) => R;
34
33
  err: (error: E) => R;
@@ -36,24 +35,23 @@ declare class Ok<T, E = never> {
36
35
  }
37
36
  /**
38
37
  * Error result variant.
39
- * @template T Success type (phantom - for type unification).
40
- * @template E Error value type.
38
+ * @template T Success type (phantom – for type unification)
39
+ * @template E Error value type
41
40
  */
42
41
  declare class Err<T, E> {
43
42
  readonly error: E;
44
43
  readonly ok = false;
45
44
  constructor(error: E);
46
- /** No-op on Err, returns self with new value type. */
45
+ /** Returns this `Err` unchanged, with a new value type. */
47
46
  map<U>(_fn: (value: T) => U): Err<U, E>;
48
47
  /** Transforms the error value. */
49
48
  mapError<E2>(fn: (error: E) => E2): Err<T, E2>;
50
- /** No-op on Err, returns self with widened error type. */
49
+ /** Returns this `Err` unchanged, with a widened error type. */
51
50
  andThen<U, E2>(_fn: (value: T) => Result<U, E2>): Err<U, E | E2>;
52
- /** Throws an error with the given message. */
53
51
  unwrap(message?: string): never;
52
+ unwrapErr(_message?: string): E;
54
53
  /** Returns the fallback value. */
55
54
  unwrapOr<U>(fallback: U): T | U;
56
- /** Pattern matches on the Result. */
57
55
  match<R>(handlers: {
58
56
  ok: (value: T) => R;
59
57
  err: (error: E) => R;
package/dist/result.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  //#region src/result.ts
2
2
  /**
3
3
  * Successful result variant.
4
- * @template T Success value type.
5
- * @template E Error type (phantom - for type unification).
4
+ * @template T Success value type
5
+ * @template E Error type (phantom – for type unification)
6
6
  */
7
7
  var Ok = class Ok {
8
8
  constructor(value) {
@@ -13,38 +13,39 @@ var Ok = class Ok {
13
13
  map(fn) {
14
14
  return new Ok(fn(this.value));
15
15
  }
16
- /** No-op on Ok, returns self with new error type. */
16
+ /** Returns this `Ok` unchanged, with a new error type. */
17
17
  mapError(_fn) {
18
18
  return this;
19
19
  }
20
- /** Chains a Result-returning function. */
20
+ /** Chains a `Result`-returning function. */
21
21
  andThen(fn) {
22
22
  return fn(this.value);
23
23
  }
24
- /** Extracts the value. */
25
24
  unwrap(_message) {
26
25
  return this.value;
27
26
  }
27
+ unwrapErr(message) {
28
+ throw new Error(message ?? `unwrapErr called on Ok: ${String(this.value)}`);
29
+ }
28
30
  /** Returns the value, ignoring the fallback. */
29
31
  unwrapOr(_fallback) {
30
32
  return this.value;
31
33
  }
32
- /** Pattern matches on the Result. */
33
34
  match(handlers) {
34
35
  return handlers.ok(this.value);
35
36
  }
36
37
  };
37
38
  /**
38
39
  * Error result variant.
39
- * @template T Success type (phantom - for type unification).
40
- * @template E Error value type.
40
+ * @template T Success type (phantom – for type unification)
41
+ * @template E Error value type
41
42
  */
42
43
  var Err = class Err {
43
44
  constructor(error) {
44
45
  this.ok = false;
45
46
  this.error = error;
46
47
  }
47
- /** No-op on Err, returns self with new value type. */
48
+ /** Returns this `Err` unchanged, with a new value type. */
48
49
  map(_fn) {
49
50
  return this;
50
51
  }
@@ -52,19 +53,20 @@ var Err = class Err {
52
53
  mapError(fn) {
53
54
  return new Err(fn(this.error));
54
55
  }
55
- /** No-op on Err, returns self with widened error type. */
56
+ /** Returns this `Err` unchanged, with a widened error type. */
56
57
  andThen(_fn) {
57
58
  return this;
58
59
  }
59
- /** Throws an error with the given message. */
60
60
  unwrap(message) {
61
- throw new Error(message ?? `Unwrap called on Err: ${String(this.error)}`);
61
+ throw new Error(message ?? `unwrap called on Err: ${String(this.error)}`);
62
+ }
63
+ unwrapErr(_message) {
64
+ return this.error;
62
65
  }
63
66
  /** Returns the fallback value. */
64
67
  unwrapOr(fallback) {
65
68
  return fallback;
66
69
  }
67
- /** Pattern matches on the Result. */
68
70
  match(handlers) {
69
71
  return handlers.err(this.error);
70
72
  }
package/dist/string.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  //#region src/string.d.ts
2
2
  declare const TEMPLATE_PLACEHOLDER_RE: RegExp;
3
3
  /**
4
- * Simple template engine to replace variables in a string.
4
+ * Replaces `{name}` placeholders in a string with the matching variable.
5
5
  *
6
6
  * @remarks
7
7
  * Only own properties of `variables` are substituted, so placeholders like
package/dist/string.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  const URL_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
3
3
  const TEMPLATE_PLACEHOLDER_RE = /\{(\w+)\}/g;
4
4
  /**
5
- * Simple template engine to replace variables in a string.
5
+ * Replaces `{name}` placeholders in a string with the matching variable.
6
6
  *
7
7
  * @remarks
8
8
  * Only own properties of `variables` are substituted, so placeholders like
package/dist/types.d.mts CHANGED
@@ -1,11 +1,9 @@
1
1
  //#region src/types.d.ts
2
- type AutocompletableString = string & {};
3
- type LooseAutocomplete<T extends string> = T | AutocompletableString;
4
- /** Also commonly referred to as `Prettify` */
2
+ /** Also commonly referred to as `Prettify`. */
5
3
  type UnifyIntersection<T> = { [K in keyof T]: T[K]; } & {};
6
4
  declare const brand: unique symbol;
7
5
  type BrandedType<T, B> = T & {
8
6
  [brand]: B;
9
7
  };
10
8
  //#endregion
11
- export { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection };
9
+ export { BrandedType, UnifyIntersection };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "utilful",
3
3
  "type": "module",
4
- "version": "3.0.1",
4
+ "version": "3.1.0",
5
5
  "packageManager": "pnpm@11.15.1",
6
6
  "description": "A collection of TypeScript utilities",
7
7
  "author": "Johann Schopplich <hello@johannschopplich.com>",
@@ -14,6 +14,15 @@
14
14
  "bugs": {
15
15
  "url": "https://github.com/johannschopplich/utilful/issues"
16
16
  },
17
+ "keywords": [
18
+ "csv",
19
+ "defu",
20
+ "emitter",
21
+ "result",
22
+ "typescript",
23
+ "url",
24
+ "utilities"
25
+ ],
17
26
  "sideEffects": false,
18
27
  "exports": {
19
28
  ".": {