@ditojs/utils 2.86.0 → 2.87.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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@ditojs/utils",
3
- "version": "2.86.0",
3
+ "version": "2.87.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Utility Functions – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
- "repository": "https://github.com/ditojs/dito/tree/master/packages/utils",
6
+ "repository": "https://github.com/ditojs/dito/tree/main/packages/utils",
7
7
  "author": "Jürg Lehni <juerg@scratchdisk.com> (http://scratchdisk.com)",
8
8
  "license": "MIT",
9
9
  "main": "./src/index.js",
@@ -38,5 +38,5 @@
38
38
  "devDependencies": {
39
39
  "typescript": "^5.9.3"
40
40
  },
41
- "gitHead": "92bc92fbb077de992fe1422adee414b8a9967c09"
41
+ "gitHead": "cbd05e604a1d212dfc91f9686b43bef75815a409"
42
42
  }
package/types/index.d.ts CHANGED
@@ -3,6 +3,11 @@
3
3
 
4
4
  /* ---------------------------------- base ---------------------------------- */
5
5
 
6
+ /**
7
+ * Re-export of `Array.isArray` as a type guard.
8
+ */
9
+ export const isArray: (arg: any) => arg is any[]
10
+
6
11
  /**
7
12
  * Determines whether both supplied values are the same value, using the
8
13
  * SameValue algorithm.
@@ -13,12 +18,17 @@ export function is(value1: any, value2: any): boolean
13
18
  /**
14
19
  * Determines whether the supplied value is a plain object.
15
20
  */
16
- export function isPlainObject(arg: any): boolean
21
+ export function isPlainObject(arg: any): arg is Record<string, unknown>
22
+
23
+ /**
24
+ * Determines whether the supplied value is a plain `Array` instance.
25
+ */
26
+ export function isPlainArray(arg: any): arg is any[]
17
27
 
18
28
  /**
19
29
  * Determines whether the supplied value is an object.
20
30
  */
21
- export function isObject(arg: any): boolean
31
+ export function isObject(arg: any): arg is Record<string, unknown>
22
32
 
23
33
  /**
24
34
  * Determines whether the supplied value is a function.
@@ -40,6 +50,11 @@ export function isString(arg: any): arg is string | String
40
50
  */
41
51
  export function isBoolean(arg: any): arg is boolean
42
52
 
53
+ /**
54
+ * Determines whether the supplied value is an ES module object.
55
+ */
56
+ export function isModule(arg: any): boolean
57
+
43
58
  /**
44
59
  * Determines whether the supplied value is a Date object.
45
60
  */
@@ -58,24 +73,26 @@ export function isPromise(arg: any): arg is Promise<any>
58
73
  /**
59
74
  * Determines whether the supplied value is an integer.
60
75
  */
61
- export function isInteger(arg: any): boolean
76
+ export function isInteger(arg: any): arg is number
62
77
 
63
78
  /**
64
79
  * Determines whether the supplied value is an async function.
65
80
  */
66
- export function isAsync(arg: any): boolean
81
+ export function isAsync(
82
+ arg: any
83
+ ): arg is (...args: any[]) => Promise<any>
67
84
 
68
85
  /**
69
86
  * Determines whether the supplied value is array like, i.e. it has a length
70
87
  • property between `0` and `Number.MAX_SAFE_INTEGER` and is not a function.
71
88
  */
72
- export function isArrayLike(arg: any): arg is any[]
89
+ export function isArrayLike(arg: any): arg is ArrayLike<any>
73
90
 
74
91
  /**
75
92
  * Determines whether the supplied value can be considered empty,
76
93
  * i.e. undefined, null, an empty string, or an object without properties.
77
94
  */
78
- export function isEmpty(o: any): boolean
95
+ export function isEmpty(arg: any): boolean
79
96
 
80
97
  /**
81
98
  * Returns the supplied value as an object. The most frequent use case is to
@@ -84,7 +101,9 @@ export function isEmpty(o: any): boolean
84
101
  *
85
102
  * @see {@link https://2ality.com/2011/04/javascript-converting-any-value-to.html JavaScript: converting any value to an object}
86
103
  */
87
- export function asObject<O extends {}, T extends any>(arg: T): T & O
104
+ export function asObject<T>(
105
+ arg: T
106
+ ): T extends null | undefined ? T : T & Object
88
107
 
89
108
  /**
90
109
  * Returns the supplied value as an array.
@@ -93,7 +112,9 @@ export function asObject<O extends {}, T extends any>(arg: T): T & O
93
112
  * the supplied value is undefined, an empty array is returned. Otherwise an
94
113
  * array is returned containing the supplied value as its only element.
95
114
  */
96
- export function asArray<T>(o: T): T extends any[] ? T : T[]
115
+ export function asArray<T>(
116
+ arg: T
117
+ ): T extends any[] ? T : Exclude<T, undefined>[]
97
118
 
98
119
  /**
99
120
  * Returns the supplied value as a function.
@@ -102,7 +123,7 @@ export function asArray<T>(o: T): T extends any[] ? T : T[]
102
123
  * Otherwise a function is returned that returns the value when called.
103
124
  */
104
125
  export function asFunction<T>(
105
- o: T
126
+ arg: T
106
127
  ): T extends Function ? T : () => T
107
128
 
108
129
  /* --------------------------------- object --------------------------------- */
@@ -132,7 +153,7 @@ export function clone<T>(value: T, options?: {
132
153
  /**
133
154
  * Optional callback to process the cloned value.
134
155
  */
135
- processValue?: <S>(value: T) => S
156
+ processValue?: (value: T) => T | undefined | void
136
157
  }): T
137
158
 
138
159
  /**
@@ -155,7 +176,12 @@ export function groupBy<T, K extends string | number | symbol>(
155
176
  callback: (item: T) => K
156
177
  ): Record<K, T[]>
157
178
 
158
- // TODO: document mergeDeeply
179
+ /**
180
+ * Recursively merges multiple source objects into the target object. For
181
+ * arrays, merges objects at the same indices and concatenates
182
+ * non-mergeable values. Does not override root-level values with
183
+ * nullish values.
184
+ */
159
185
  export function mergeDeeply<ArgA, ArgB>(a: ArgA, b: ArgB): ArgA & ArgB
160
186
  export function mergeDeeply<ArgA, ArgB, ArgC>(
161
187
  a: ArgA,
@@ -177,7 +203,12 @@ export function mergeDeeply<ArgA, ArgB, ArgC, ArgD, ArgE>(
177
203
  ): ArgA & ArgB & ArgC & ArgD & ArgE
178
204
  export function mergeDeeply(...args: any[]): any
179
205
 
180
- // TODO: document assignDeeply
206
+ /**
207
+ * Recursively assigns values from source objects into the target object.
208
+ * Similar to `mergeDeeply` but without array concatenation — overwrites
209
+ * array values at the same indices instead. Does not override
210
+ * root-level values with nullish values.
211
+ */
181
212
  export function assignDeeply<ArgA, ArgB>(a: ArgA, b: ArgB): ArgA & ArgB
182
213
  export function assignDeeply<ArgA, ArgB, ArgC>(
183
214
  a: ArgA,
@@ -236,12 +267,17 @@ export function pickBy<T extends Dictionary<any>, K extends keyof T[keyof T]>(
236
267
  ): Partial<T>
237
268
  export function pickBy<T extends Dictionary<any>>(
238
269
  object: T,
239
- callback?: (value: T[keyof T], key: keyof T, object: T) => any
270
+ callback: (value: T[keyof T], key: keyof T, object: T) => any
240
271
  ): Partial<T>
241
272
 
273
+ /**
274
+ * Transforms object keys by applying a callback function to each
275
+ * key-value pair. Returns a new object with the transformed keys and
276
+ * original values.
277
+ */
242
278
  export function mapKeys<T extends Dictionary<any>, K extends keyof any>(
243
279
  object: T,
244
- callback?: (key: keyof T, value: T[keyof T], object: T) => K
280
+ callback: (key: keyof T, value: T[keyof T], object: T) => K
245
281
  ): Record<K, T[keyof T]>
246
282
 
247
283
  /**
@@ -257,11 +293,20 @@ export function mapValues<
257
293
  ): Record<keyof T, T[keyof T][K]>
258
294
  export function mapValues<T extends Dictionary<any>, K>(
259
295
  object: T,
260
- callback?: (value: T[keyof T], key: keyof T, object: T) => K
296
+ callback: (value: T[keyof T], key: keyof T, object: T) => K
261
297
  ): Record<keyof T, K>
262
298
 
263
299
  /* -------------------------------- promise -------------------------------- */
264
300
 
301
+ /**
302
+ * Maps an async callback over an array with controlled concurrency.
303
+ * Executes all promises in parallel by default, or limits concurrent
304
+ * execution when the `concurrency` option is set.
305
+ *
306
+ * @param input An array or a promise that resolves to an array.
307
+ * @param callback Async function called for each element.
308
+ * @param options.concurrency Max concurrent promises (0 = unlimited).
309
+ */
265
310
  export function mapConcurrently<T, R>(
266
311
  input: Promise<T[]> | T[],
267
312
  callback: (value: T, index: number, array: T[]) => Promise<R> | R,
@@ -270,6 +315,13 @@ export function mapConcurrently<T, R>(
270
315
  }
271
316
  ): Promise<R[]>
272
317
 
318
+ /**
319
+ * Maps an async callback over an array sequentially, waiting for each
320
+ * promise to resolve before processing the next element.
321
+ *
322
+ * @param input An array or a promise that resolves to an array.
323
+ * @param callback Async function called for each element.
324
+ */
273
325
  export function mapSequentially<T, R>(
274
326
  input: Promise<T[]> | T[],
275
327
  callback: (value: T, index: number) => Promise<R> | R
@@ -403,7 +455,7 @@ export interface TimeFormat {
403
455
  value: string,
404
456
  type: Intl.DateTimeFormatPartTypes,
405
457
  options: Omit<TimeFormat, 'format'>
406
- ) => string
458
+ ) => string | undefined
407
459
  }
408
460
 
409
461
  export interface DateFormat {
@@ -426,7 +478,7 @@ export interface DateFormat {
426
478
  value: string,
427
479
  type: Intl.DateTimeFormatPartTypes,
428
480
  options: Omit<DateFormat, 'format'>
429
- ) => string
481
+ ) => string | undefined
430
482
  }
431
483
 
432
484
  export interface NumberFormat extends Intl.NumberFormatOptions {
@@ -437,11 +489,20 @@ export interface NumberFormat extends Intl.NumberFormatOptions {
437
489
  ) => string | undefined
438
490
  }
439
491
 
492
+ /**
493
+ * Default format options for number, date, and time formatting.
494
+ */
495
+ export const defaultFormats: {
496
+ number: NumberFormat
497
+ date: DateFormat
498
+ time: TimeFormat
499
+ }
500
+
440
501
  /**
441
502
  * Formats the value as a string.
442
503
  */
443
504
  export function format(
444
- value: Date,
505
+ value: Date | string | number | null | undefined,
445
506
  options?: {
446
507
  /**
447
508
  * @default 'en-US'
@@ -450,8 +511,13 @@ export function format(
450
511
  date?: boolean | DateFormat
451
512
  time?: boolean | TimeFormat
452
513
  number?: boolean | NumberFormat
514
+ defaults?: {
515
+ number?: NumberFormat
516
+ date?: DateFormat
517
+ time?: TimeFormat
518
+ }
453
519
  }
454
- ): string
520
+ ): string | null | undefined
455
521
  /**
456
522
  * Formats a date value as a string. If the value is not a Date,
457
523
  * attempts to convert it to a Date first.
@@ -472,7 +538,7 @@ export function formatDate(
472
538
  */
473
539
  time?: boolean | TimeFormat
474
540
  }
475
- ): string
541
+ ): string | null | undefined
476
542
  /* -------------------------------- function -------------------------------- */
477
543
 
478
544
  /**
@@ -646,7 +712,7 @@ export function toCallback<T1, T2>(
646
712
  ): (arg1: T1, arg2: T2, callback: (err: any) => void) => void
647
713
  export function toCallback<T1, T2, R>(
648
714
  fn: (arg1: T1, arg2: T2) => Promise<R>
649
- ): (arg1: T1, arg2: T2, callback: (err: any | null, result: R) => void) => void
715
+ ): (arg1: T1, arg2: T2, callback: (err: any, result: R) => void) => void
650
716
  export function toCallback<T1, T2, T3>(
651
717
  fn: (arg1: T1, arg2: T2, arg3: T3) => Promise<void>
652
718
  ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: any) => void) => void
@@ -656,7 +722,7 @@ export function toCallback<T1, T2, T3, R>(
656
722
  arg1: T1,
657
723
  arg2: T2,
658
724
  arg3: T3,
659
- callback: (err: any | null, result: R) => void
725
+ callback: (err: any, result: R) => void
660
726
  ) => void
661
727
  export function toCallback<T1, T2, T3, T4>(
662
728
  fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<void>
@@ -674,7 +740,7 @@ export function toCallback<T1, T2, T3, T4, R>(
674
740
  arg2: T2,
675
741
  arg3: T3,
676
742
  arg4: T4,
677
- callback: (err: any | null, result: R) => void
743
+ callback: (err: any, result: R) => void
678
744
  ) => void
679
745
  export function toCallback<T1, T2, T3, T4, T5>(
680
746
  fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<void>
@@ -694,7 +760,7 @@ export function toCallback<T1, T2, T3, T4, T5, R>(
694
760
  arg3: T3,
695
761
  arg4: T4,
696
762
  arg5: T5,
697
- callback: (err: any | null, result: R) => void
763
+ callback: (err: any, result: R) => void
698
764
  ) => void
699
765
  export function toCallback<T1, T2, T3, T4, T5, T6>(
700
766
  fn: (
@@ -723,9 +789,18 @@ export function toCallback<T1, T2, T3, T4, T5, T6, R>(
723
789
  arg4: T4,
724
790
  arg5: T5,
725
791
  arg6: T6,
726
- callback: (err: any | null, result: R) => void
792
+ callback: (err: any, result: R) => void
727
793
  ) => void
728
794
 
795
+ /**
796
+ * Creates a Node.js-style error-first callback that resolves or rejects
797
+ * a promise. If the error argument is truthy, rejects the promise;
798
+ * otherwise resolves with the result.
799
+ *
800
+ * @param resolve Promise resolve function.
801
+ * @param reject Promise reject function.
802
+ * @returns Callback with signature `(err, res) => void`.
803
+ */
729
804
  export function toPromiseCallback<T, R>(
730
805
  resolve: (value: T) => void,
731
806
  reject: (reason: R) => void
@@ -741,6 +816,7 @@ export function toPromiseCallback<T, R>(
741
816
  * @param path The data path (supports wildcards).
742
817
  * @param handleError Optional error handler called when path is invalid.
743
818
  * @returns Object with normalized paths as keys and values at those paths.
819
+ * @see {@link parseDataPath} for supported path formats.
744
820
  */
745
821
  export function getEntriesAtDataPath(
746
822
  obj: any,
@@ -748,15 +824,40 @@ export function getEntriesAtDataPath(
748
824
  handleError?: (obj: any, part: string, index: number) => any
749
825
  ): Record<string, any>
750
826
 
827
+ /**
828
+ * Retrieves a value from a nested object or array at a given path.
829
+ * Supports property access notation (`obj.arr[0].prop`), JSON pointers
830
+ * (`/obj/arr/0/prop`), and wildcard matching (`*` for shallow, `**`
831
+ * for deep recursive).
832
+ *
833
+ * @param obj The object or array to retrieve from.
834
+ * @param path The data path (multiple formats supported).
835
+ * @param handleError Optional error handler called when the path is
836
+ * invalid, with `(obj, part, index)`.
837
+ * @see {@link parseDataPath} for supported path formats.
838
+ */
751
839
  export function getValueAtDataPath(
752
840
  obj: any,
753
841
  path: OrArrayOf<string>,
754
- handleError?: (obj: any, part: string, index: number) => void
842
+ handleError?: (obj: any, part: string, index: number) => any
755
843
  ): any
756
844
 
845
+ /**
846
+ * Normalizes a data path to a standard format using forward slashes.
847
+ * Converts property access notation, JSON pointers, and relative paths
848
+ * to normalized relative path format. Resolves relative tokens (`..`
849
+ * and `.`).
850
+ * @see {@link parseDataPath} for supported path formats.
851
+ */
757
852
  export function normalizeDataPath(path: OrArrayOf<string>): string
758
853
 
759
- export function parseDataPath(path: OrArrayOf<string>): string
854
+ /**
855
+ * Parses a data path string or array into an array of path segments.
856
+ * Supports property access notation (`obj.arr[0].prop`), JSON pointers
857
+ * (`/obj/arr/0/prop`), and relative paths. Always returns a new array
858
+ * to prevent mutation.
859
+ */
860
+ export function parseDataPath(path: OrArrayOf<string>): string[]
760
861
 
761
862
  /**
762
863
  * Sets multiple values at data paths from an entries object.
@@ -764,12 +865,24 @@ export function parseDataPath(path: OrArrayOf<string>): string
764
865
  * @param obj The object to modify.
765
866
  * @param entries Object with data paths as keys and values to set.
766
867
  * @returns The modified object.
868
+ * @see {@link parseDataPath} for supported path formats.
767
869
  */
768
870
  export function setDataPathEntries<O>(
769
871
  obj: O,
770
872
  entries: Record<string, any>
771
873
  ): O
772
874
 
875
+ /**
876
+ * Sets a value in a nested object or array at a given path. Parses the
877
+ * path, navigates to the parent location, and assigns the value.
878
+ * Mutates the original object.
879
+ *
880
+ * @param obj The object or array to set the value on.
881
+ * @param path The data path to the target location.
882
+ * @param value The value to set.
883
+ * @returns The modified object.
884
+ * @see {@link parseDataPath} for supported path formats.
885
+ */
773
886
  export function setValueAtDataPath<O>(
774
887
  obj: O,
775
888
  path: OrArrayOf<string>,
@@ -792,19 +905,19 @@ export function mixin<T extends new (...args: any[]) => any>(
792
905
  /* ---------------------------------- html ---------------------------------- */
793
906
 
794
907
  /**
795
- * Escapes quotes, ampersands, and smaller/greater than signs (`&<>'"`).
908
+ * Escapes double quotes, ampersands, and angle brackets (`"&<>`).
796
909
 
797
910
  * @param html The html to escape.
798
911
  * @returns The newly escaped html.
799
912
  */
800
- export function escapeHtml(html: string): string
913
+ export function escapeHtml(html: string | null | undefined): string
801
914
 
802
915
  /**
803
916
  * Strips HTML tags from the string.
804
917
  * @param html The string to strip.
805
918
  * @returns The newly stripped string.
806
919
  */
807
- export function stripHtml(html: string): string
920
+ export function stripHtml(html: string | null | undefined): string
808
921
 
809
922
  /**
810
923
  * Strips HTML tags from the string.
@@ -812,12 +925,9 @@ export function stripHtml(html: string): string
812
925
  * @returns The newly stripped string.
813
926
  * @deprecated Use stripHtml() instead
814
927
  */
815
- export function stripTags(html: string): string
928
+ export function stripTags(html: string | null | undefined): string
816
929
 
817
930
  /* -------------------------- typescript utilities -------------------------- */
818
- type PropertyName = string | number | symbol
819
- type NotVoid = {} | null | undefined
820
- type List<T> = ArrayLike<T>
821
931
  export interface ArrayLike<T> {
822
932
  readonly length: number
823
933
  readonly [n: number]: T
@@ -0,0 +1,172 @@
1
+ import { assertType, describe, expectTypeOf, it } from 'vitest'
2
+ import type {
3
+ isArray,
4
+ isPlainObject,
5
+ isFunction,
6
+ isNumber,
7
+ isString,
8
+ isBoolean,
9
+ isDate,
10
+ isRegExp,
11
+ isPromise,
12
+ isAsync,
13
+ isArrayLike,
14
+ asObject,
15
+ asArray,
16
+ asFunction
17
+ } from '../index.d.ts'
18
+
19
+ describe('type guards', () => {
20
+ it('isArray narrows unknown to any[]', () => {
21
+ const guard = {} as typeof isArray
22
+ const val: unknown = []
23
+ if (guard(val)) {
24
+ expectTypeOf(val).toEqualTypeOf<any[]>()
25
+ }
26
+ })
27
+
28
+ it('isPlainObject narrows union to record', () => {
29
+ const guard = {} as typeof isPlainObject
30
+ const val: string | Record<string, unknown> = {} as any
31
+ if (guard(val)) {
32
+ expectTypeOf(val).not.toBeAny()
33
+ expectTypeOf(val).toEqualTypeOf<Record<string, unknown>>()
34
+ }
35
+ })
36
+
37
+ it('isFunction narrows to callable', () => {
38
+ const guard = {} as typeof isFunction
39
+ const val: string | (() => void) = {} as any
40
+ if (guard(val)) {
41
+ expectTypeOf(val).not.toBeAny()
42
+ expectTypeOf(val).toBeCallableWith()
43
+ }
44
+ })
45
+
46
+ it('isNumber narrows union', () => {
47
+ const guard = {} as typeof isNumber
48
+ const val: string | number = {} as any
49
+ if (guard(val)) {
50
+ expectTypeOf(val).not.toBeAny()
51
+ expectTypeOf(val).toBeNumber()
52
+ }
53
+ })
54
+
55
+ it('isString narrows union', () => {
56
+ const guard = {} as typeof isString
57
+ const val: number | string = {} as any
58
+ if (guard(val)) {
59
+ expectTypeOf(val).not.toBeAny()
60
+ assertType<string | String>(val)
61
+ }
62
+ })
63
+
64
+ it('isBoolean narrows union', () => {
65
+ const guard = {} as typeof isBoolean
66
+ const val: string | boolean = {} as any
67
+ if (guard(val)) {
68
+ expectTypeOf(val).not.toBeAny()
69
+ expectTypeOf(val).toBeBoolean()
70
+ }
71
+ })
72
+
73
+ it('isDate narrows to Date', () => {
74
+ const guard = {} as typeof isDate
75
+ const val: string | Date = {} as any
76
+ if (guard(val)) {
77
+ expectTypeOf(val).not.toBeAny()
78
+ expectTypeOf(val).toEqualTypeOf<Date>()
79
+ }
80
+ })
81
+
82
+ it('isRegExp narrows to RegExp', () => {
83
+ const guard = {} as typeof isRegExp
84
+ const val: string | RegExp = {} as any
85
+ if (guard(val)) {
86
+ expectTypeOf(val).not.toBeAny()
87
+ expectTypeOf(val).toEqualTypeOf<RegExp>()
88
+ }
89
+ })
90
+
91
+ it('isPromise narrows to Promise', () => {
92
+ const guard = {} as typeof isPromise
93
+ const val: string | Promise<number> = {} as any
94
+ if (guard(val)) {
95
+ expectTypeOf(val).not.toBeAny()
96
+ assertType<Promise<any>>(val)
97
+ }
98
+ })
99
+
100
+ it('isAsync narrows to async function', () => {
101
+ const guard = {} as typeof isAsync
102
+ const val: (() => void) | (() => Promise<string>) = {} as any
103
+ if (guard(val)) {
104
+ expectTypeOf(val).not.toBeAny()
105
+ assertType<(...args: any[]) => Promise<any>>(val)
106
+ }
107
+ })
108
+
109
+ it('isArrayLike narrows to ArrayLike', () => {
110
+ const guard = {} as typeof isArrayLike
111
+ const val: string | ArrayLike<number> = {} as any
112
+ if (guard(val)) {
113
+ expectTypeOf(val).not.toBeAny()
114
+ expectTypeOf(val).toHaveProperty('length')
115
+ }
116
+ })
117
+ })
118
+
119
+ describe('asObject', () => {
120
+ it('preserves object types', () => {
121
+ const fn = {} as typeof asObject
122
+ const obj = { a: 1 }
123
+ const result = fn(obj)
124
+ expectTypeOf(result).not.toBeAny()
125
+ expectTypeOf(result).toHaveProperty('a')
126
+ })
127
+
128
+ it('returns null/undefined as-is', () => {
129
+ const fn = {} as typeof asObject
130
+ expectTypeOf(fn(null)).toBeNull()
131
+ expectTypeOf(fn(undefined)).toBeUndefined()
132
+ })
133
+ })
134
+
135
+ describe('asArray', () => {
136
+ it('passes arrays through unchanged', () => {
137
+ const fn = {} as typeof asArray
138
+ const arr = [1, 2, 3]
139
+ const result = fn(arr)
140
+ expectTypeOf(result).not.toBeAny()
141
+ expectTypeOf(result).toEqualTypeOf<number[]>()
142
+ })
143
+
144
+ it('wraps non-array in array', () => {
145
+ const fn = {} as typeof asArray
146
+ const result = fn('hello')
147
+ expectTypeOf(result).not.toBeAny()
148
+ expectTypeOf(result).toEqualTypeOf<string[]>()
149
+ })
150
+
151
+ it('returns empty array for undefined', () => {
152
+ const fn = {} as typeof asArray
153
+ expectTypeOf(fn(undefined)).toEqualTypeOf<never[]>()
154
+ })
155
+ })
156
+
157
+ describe('asFunction', () => {
158
+ it('passes functions through unchanged', () => {
159
+ const fn = {} as typeof asFunction
160
+ const cb = (x: number) => x * 2
161
+ const result = fn(cb)
162
+ expectTypeOf(result).not.toBeAny()
163
+ expectTypeOf(result).toEqualTypeOf<(x: number) => number>()
164
+ })
165
+
166
+ it('wraps non-function in thunk', () => {
167
+ const fn = {} as typeof asFunction
168
+ const result = fn(42)
169
+ expectTypeOf(result).not.toBeAny()
170
+ expectTypeOf(result).toEqualTypeOf<() => number>()
171
+ })
172
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import type {
3
+ getValueAtDataPath,
4
+ getEntriesAtDataPath,
5
+ setValueAtDataPath,
6
+ setDataPathEntries,
7
+ normalizeDataPath,
8
+ parseDataPath
9
+ } from '../index.d.ts'
10
+
11
+ describe('getValueAtDataPath', () => {
12
+ it('error handler receives correct params', () => {
13
+ const fn = {} as typeof getValueAtDataPath
14
+ fn({}, 'a.b', (obj, part, index) => {
15
+ expectTypeOf(part).not.toBeAny()
16
+ expectTypeOf(part).toBeString()
17
+ expectTypeOf(index).not.toBeAny()
18
+ expectTypeOf(index).toBeNumber()
19
+ })
20
+ })
21
+ })
22
+
23
+ describe('getEntriesAtDataPath', () => {
24
+ it('error handler receives correct params', () => {
25
+ const fn = {} as typeof getEntriesAtDataPath
26
+ fn({}, 'items.*', (obj, part, index) => {
27
+ expectTypeOf(part).not.toBeAny()
28
+ expectTypeOf(part).toBeString()
29
+ expectTypeOf(index).not.toBeAny()
30
+ expectTypeOf(index).toBeNumber()
31
+ })
32
+ })
33
+ })
34
+
35
+ describe('setValueAtDataPath', () => {
36
+ it('preserves the object type', () => {
37
+ const fn = {} as typeof setValueAtDataPath
38
+ const obj = { a: { b: 1 } }
39
+ const result = fn(obj, 'a.b', 2)
40
+ expectTypeOf(result).not.toBeAny()
41
+ expectTypeOf(result).toEqualTypeOf<typeof obj>()
42
+ })
43
+ })
44
+
45
+ describe('setDataPathEntries', () => {
46
+ it('preserves the object type', () => {
47
+ const fn = {} as typeof setDataPathEntries
48
+ const obj = { x: 1, y: 2 }
49
+ const result = fn(obj, { x: 10 })
50
+ expectTypeOf(result).not.toBeAny()
51
+ expectTypeOf(result).toEqualTypeOf<typeof obj>()
52
+ })
53
+ })
54
+
55
+ describe('parseDataPath', () => {
56
+ it('returns string array from string or array input', () => {
57
+ const fn = {} as typeof parseDataPath
58
+ const fromString = fn('a.b.c')
59
+ const fromArray = fn(['a', 'b'])
60
+ expectTypeOf(fromString).not.toBeAny()
61
+ expectTypeOf(fromString).toEqualTypeOf<string[]>()
62
+ expectTypeOf(fromArray).toEqualTypeOf<string[]>()
63
+ })
64
+ })
65
+
66
+ describe('normalizeDataPath', () => {
67
+ it('returns string from string or array input', () => {
68
+ const fn = {} as typeof normalizeDataPath
69
+ const fromString = fn('a.b')
70
+ const fromArray = fn(['a', 'b'])
71
+ expectTypeOf(fromString).not.toBeAny()
72
+ expectTypeOf(fromString).toBeString()
73
+ expectTypeOf(fromArray).toBeString()
74
+ })
75
+ })
@@ -0,0 +1,137 @@
1
+ import { assertType, describe, expectTypeOf, it } from 'vitest'
2
+ import type {
3
+ debounce,
4
+ debounceAsync,
5
+ toAsync,
6
+ toCallback,
7
+ toPromiseCallback
8
+ } from '../index.d.ts'
9
+
10
+ describe('debounce', () => {
11
+ it('preserves original function signature', () => {
12
+ const fn = {} as typeof debounce
13
+ const original = (x: number, y: string) => x + y.length
14
+ const debounced = fn(original, 100)
15
+ expectTypeOf(debounced).not.toBeAny()
16
+ expectTypeOf(debounced).toBeCallableWith(1, 'hello')
17
+ expectTypeOf(debounced(1, 'a')).toBeNumber()
18
+ })
19
+
20
+ it('adds cancel method', () => {
21
+ const fn = {} as typeof debounce
22
+ const debounced = fn(() => {}, 100)
23
+ expectTypeOf(debounced.cancel).not.toBeAny()
24
+ expectTypeOf(debounced.cancel).toBeFunction()
25
+ expectTypeOf(debounced.cancel()).toBeBoolean()
26
+ })
27
+
28
+ it('accepts options object', () => {
29
+ const fn = {} as typeof debounce
30
+ fn((x: number) => x, { delay: 100, immediate: true })
31
+ })
32
+
33
+ it('rejects wrong argument types', () => {
34
+ const fn = {} as typeof debounce
35
+ const debounced = fn((x: number) => x, 100)
36
+ // @ts-expect-error - string not assignable to number
37
+ debounced('wrong')
38
+ })
39
+ })
40
+
41
+ describe('debounceAsync', () => {
42
+ it('preserves async function signature', () => {
43
+ const fn = {} as typeof debounceAsync
44
+ const original = async (id: number) => ({
45
+ id,
46
+ name: 'test'
47
+ })
48
+ const debounced = fn(original, 200)
49
+ expectTypeOf(debounced).not.toBeAny()
50
+ expectTypeOf(debounced).toBeCallableWith(1)
51
+ expectTypeOf(debounced(1))
52
+ .resolves.toHaveProperty('name')
53
+ })
54
+ })
55
+
56
+ describe('toAsync', () => {
57
+ it('converts error-first callback to promise', () => {
58
+ const fn = {} as typeof toAsync
59
+ const readFile = (
60
+ path: string,
61
+ cb: (err: any, data: Buffer) => void
62
+ ) => {}
63
+ const asyncReadFile = fn(readFile)
64
+ expectTypeOf(asyncReadFile).not.toBeAny()
65
+ expectTypeOf(asyncReadFile).toBeCallableWith('test.txt')
66
+ expectTypeOf(asyncReadFile('x'))
67
+ .resolves.toEqualTypeOf<Buffer>()
68
+ })
69
+
70
+ it('converts void callback to promise', () => {
71
+ const fn = {} as typeof toAsync
72
+ const doThing = (cb: (err?: any) => void) => {}
73
+ const asyncDoThing = fn(doThing)
74
+ expectTypeOf(asyncDoThing).not.toBeAny()
75
+ expectTypeOf(asyncDoThing())
76
+ .resolves.toEqualTypeOf<void>()
77
+ })
78
+
79
+ it('handles multiple arguments', () => {
80
+ const fn = {} as typeof toAsync
81
+ const write = (
82
+ path: string,
83
+ data: string,
84
+ cb: (err: any, written: number) => void
85
+ ) => {}
86
+ const asyncWrite = fn(write)
87
+ expectTypeOf(asyncWrite).not.toBeAny()
88
+ expectTypeOf(asyncWrite).toBeCallableWith('f', 'data')
89
+ expectTypeOf(asyncWrite('f', 'd'))
90
+ .resolves.toBeNumber()
91
+ })
92
+ })
93
+
94
+ describe('toCallback', () => {
95
+ it('converts async fn to callback style', () => {
96
+ const fn = {} as typeof toCallback
97
+ const asyncFn = async (x: number): Promise<string> => `${x}`
98
+ const cbFn = fn(asyncFn)
99
+ expectTypeOf(cbFn).not.toBeAny()
100
+ expectTypeOf(cbFn).toBeCallableWith(
101
+ 42,
102
+ (err: any, result: string) => {}
103
+ )
104
+ })
105
+
106
+ it('handles void return', () => {
107
+ const fn = {} as typeof toCallback
108
+ const asyncFn = async (x: string): Promise<void> => {}
109
+ const cbFn = fn(asyncFn)
110
+ expectTypeOf(cbFn).not.toBeAny()
111
+ expectTypeOf(cbFn).toBeCallableWith(
112
+ 'hello',
113
+ (err: any) => {}
114
+ )
115
+ })
116
+ })
117
+
118
+ describe('toPromiseCallback', () => {
119
+ it('returns callback matching resolve/reject types', () => {
120
+ const fn = {} as typeof toPromiseCallback
121
+ const cb = fn<string, Error>(
122
+ value => {
123
+ expectTypeOf(value).not.toBeAny()
124
+ expectTypeOf(value).toBeString()
125
+ },
126
+ reason => {
127
+ expectTypeOf(reason).not.toBeAny()
128
+ expectTypeOf(reason).toEqualTypeOf<Error>()
129
+ }
130
+ )
131
+ expectTypeOf(cb).not.toBeAny()
132
+ expectTypeOf(cb).toBeCallableWith(
133
+ new Error(),
134
+ 'result'
135
+ )
136
+ })
137
+ })
@@ -0,0 +1,190 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import type {
3
+ clone,
4
+ groupBy,
5
+ mergeDeeply,
6
+ assignDeeply,
7
+ pick,
8
+ pickBy,
9
+ mapKeys,
10
+ mapValues
11
+ } from '../index.d.ts'
12
+
13
+ describe('clone', () => {
14
+ it('preserves the input type', () => {
15
+ const fn = {} as typeof clone
16
+ const obj = { a: 1, b: 'hello' }
17
+ const result = fn(obj)
18
+ expectTypeOf(result).not.toBeAny()
19
+ expectTypeOf(result).toEqualTypeOf<typeof obj>()
20
+ })
21
+
22
+ it('processValue callback receives correct type', () => {
23
+ const fn = {} as typeof clone
24
+ fn(
25
+ { x: 1 },
26
+ {
27
+ processValue(value) {
28
+ expectTypeOf(value).not.toBeAny()
29
+ expectTypeOf(value).toEqualTypeOf<{ x: number }>()
30
+ return value
31
+ }
32
+ }
33
+ )
34
+ })
35
+ })
36
+
37
+ describe('groupBy', () => {
38
+ it('infers callback param type from array', () => {
39
+ const fn = {} as typeof groupBy
40
+ const items = [
41
+ { name: 'a', category: 'x' },
42
+ { name: 'b', category: 'y' }
43
+ ]
44
+ fn(items, item => {
45
+ expectTypeOf(item).not.toBeAny()
46
+ expectTypeOf(item).toEqualTypeOf<{
47
+ name: string
48
+ category: string
49
+ }>()
50
+ return item.category
51
+ })
52
+ })
53
+
54
+ it('accepts property key shorthand', () => {
55
+ const fn = {} as typeof groupBy
56
+ const items = [{ type: 'a' as const, v: 1 }]
57
+ const result = fn(items, 'type')
58
+ expectTypeOf(result).not.toBeAny()
59
+ expectTypeOf(result).toEqualTypeOf<
60
+ Record<string, { type: 'a'; v: number }[]>
61
+ >()
62
+ })
63
+
64
+ it('works with record input', () => {
65
+ const fn = {} as typeof groupBy
66
+ const obj: Record<string, { group: string }> = {}
67
+ fn(obj, item => {
68
+ expectTypeOf(item).not.toBeAny()
69
+ expectTypeOf(item).toEqualTypeOf<{ group: string }>()
70
+ return item.group
71
+ })
72
+ })
73
+ })
74
+
75
+ describe('mergeDeeply', () => {
76
+ it('produces intersection of two args', () => {
77
+ const fn = {} as typeof mergeDeeply
78
+ const result = fn(
79
+ {} as { a: number },
80
+ {} as { b: string }
81
+ )
82
+ expectTypeOf(result).not.toBeAny()
83
+ expectTypeOf(result).toHaveProperty('a')
84
+ expectTypeOf(result).toHaveProperty('b')
85
+ })
86
+
87
+ it('produces intersection of three args', () => {
88
+ const fn = {} as typeof mergeDeeply
89
+ const result = fn(
90
+ {} as { a: number },
91
+ {} as { b: string },
92
+ {} as { c: boolean }
93
+ )
94
+ expectTypeOf(result).not.toBeAny()
95
+ expectTypeOf(result).toHaveProperty('a')
96
+ expectTypeOf(result).toHaveProperty('b')
97
+ expectTypeOf(result).toHaveProperty('c')
98
+ })
99
+ })
100
+
101
+ describe('assignDeeply', () => {
102
+ it('produces intersection of two args', () => {
103
+ const fn = {} as typeof assignDeeply
104
+ const result = fn(
105
+ {} as { a: number },
106
+ {} as { b: string }
107
+ )
108
+ expectTypeOf(result).not.toBeAny()
109
+ expectTypeOf(result).toHaveProperty('a')
110
+ expectTypeOf(result).toHaveProperty('b')
111
+ })
112
+ })
113
+
114
+ describe('pick', () => {
115
+ it('returns union of argument types', () => {
116
+ const fn = {} as typeof pick
117
+ const result = fn(
118
+ {} as number | undefined,
119
+ {} as string
120
+ )
121
+ expectTypeOf(result).not.toBeAny()
122
+ expectTypeOf(result).toEqualTypeOf<number | undefined | string>()
123
+ })
124
+ })
125
+
126
+ describe('pickBy', () => {
127
+ it('callback receives value, key, and object', () => {
128
+ const fn = {} as typeof pickBy
129
+ const obj: Record<string, number> = {}
130
+ fn(obj, (value, key, source) => {
131
+ expectTypeOf(value).not.toBeAny()
132
+ expectTypeOf(value).toBeNumber()
133
+ expectTypeOf(key).toBeString()
134
+ expectTypeOf(source).toEqualTypeOf(obj)
135
+ return value > 1
136
+ })
137
+ })
138
+
139
+ it('returns partial of input type', () => {
140
+ const fn = {} as typeof pickBy
141
+ const obj = {} as { a: number; b: number }
142
+ const result = fn(obj, () => true)
143
+ expectTypeOf(result).not.toBeAny()
144
+ expectTypeOf(result).toEqualTypeOf<Partial<{ a: number; b: number }>>()
145
+ })
146
+ })
147
+
148
+ describe('mapKeys', () => {
149
+ it('callback receives key, value, and object', () => {
150
+ const fn = {} as typeof mapKeys
151
+ const obj: Record<string, number> = {}
152
+ fn(obj, (key, value, source) => {
153
+ expectTypeOf(value).not.toBeAny()
154
+ expectTypeOf(key).toBeString()
155
+ expectTypeOf(value).toBeNumber()
156
+ expectTypeOf(source).toEqualTypeOf(obj)
157
+ return `prefix_${key}`
158
+ })
159
+ })
160
+ })
161
+
162
+ describe('mapValues', () => {
163
+ it('callback receives value, key, and object', () => {
164
+ const fn = {} as typeof mapValues
165
+ const obj: Record<string, number> = {}
166
+ fn(obj, (value, key, source) => {
167
+ expectTypeOf(value).not.toBeAny()
168
+ expectTypeOf(value).toBeNumber()
169
+ expectTypeOf(key).toBeString()
170
+ expectTypeOf(source).toEqualTypeOf(obj)
171
+ return String(value)
172
+ })
173
+ })
174
+
175
+ it('result values match callback return type', () => {
176
+ const fn = {} as typeof mapValues
177
+ const obj: Record<string, number> = {}
178
+ const result = fn(obj, value => String(value))
179
+ expectTypeOf(result).not.toBeAny()
180
+ expectTypeOf(result).toEqualTypeOf<Record<string, string>>()
181
+ })
182
+
183
+ it('property shorthand extracts nested value', () => {
184
+ const fn = {} as typeof mapValues
185
+ const obj: Record<string, { nested: number }> = {}
186
+ const result = fn(obj, 'nested')
187
+ expectTypeOf(result).not.toBeAny()
188
+ expectTypeOf(result).toEqualTypeOf<Record<string, number>>()
189
+ })
190
+ })
@@ -0,0 +1,66 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import type { mapConcurrently, mapSequentially } from '../index.d.ts'
3
+
4
+ describe('mapConcurrently', () => {
5
+ it('infers callback param from input array type', () => {
6
+ const fn = {} as typeof mapConcurrently
7
+ fn([1, 2, 3], async (value, index, array) => {
8
+ expectTypeOf(value).not.toBeAny()
9
+ expectTypeOf(value).toBeNumber()
10
+ expectTypeOf(index).toBeNumber()
11
+ expectTypeOf(array).toEqualTypeOf<number[]>()
12
+ return String(value)
13
+ })
14
+ })
15
+
16
+ it('result type matches callback return', () => {
17
+ const fn = {} as typeof mapConcurrently
18
+ const result = fn(
19
+ [1, 2],
20
+ async value => ({ doubled: value * 2 })
21
+ )
22
+ expectTypeOf(result)
23
+ .resolves.items.not.toBeAny()
24
+ expectTypeOf(result)
25
+ .resolves.items.toEqualTypeOf<{ doubled: number }>()
26
+ })
27
+
28
+ it('accepts promise input', () => {
29
+ const fn = {} as typeof mapConcurrently
30
+ const input = Promise.resolve(['a', 'b'])
31
+ fn(input, async value => {
32
+ expectTypeOf(value).not.toBeAny()
33
+ expectTypeOf(value).toBeString()
34
+ return value.length
35
+ })
36
+ })
37
+
38
+ it('accepts synchronous callback', () => {
39
+ const fn = {} as typeof mapConcurrently
40
+ const result = fn([1, 2], value => `${value}`)
41
+ expectTypeOf(result).resolves.items.not.toBeAny()
42
+ expectTypeOf(result).resolves.items.toBeString()
43
+ })
44
+ })
45
+
46
+ describe('mapSequentially', () => {
47
+ it('infers callback param from input array type', () => {
48
+ const fn = {} as typeof mapSequentially
49
+ fn(['a', 'b'], async (value, index) => {
50
+ expectTypeOf(value).not.toBeAny()
51
+ expectTypeOf(value).toBeString()
52
+ expectTypeOf(index).toBeNumber()
53
+ return value.length
54
+ })
55
+ })
56
+
57
+ it('result type matches callback return', () => {
58
+ const fn = {} as typeof mapSequentially
59
+ const result = fn(
60
+ [{ id: 1 }],
61
+ async item => item.id
62
+ )
63
+ expectTypeOf(result).resolves.items.not.toBeAny()
64
+ expectTypeOf(result).resolves.items.toBeNumber()
65
+ })
66
+ })