@zudojs/types 0.1.0 → 1.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -7
  3. package/dist/runtime/index.d.ts +11 -1
  4. package/dist/runtime/index.js +10 -1
  5. package/dist/runtime/runtime.core.d.ts +49 -11
  6. package/dist/runtime/runtime.core.js +35 -52
  7. package/dist/runtime/runtime.int.d.ts +31 -0
  8. package/dist/runtime/runtime.int.js +54 -0
  9. package/dist/runtime/runtime.seeded.d.ts +31 -0
  10. package/dist/runtime/runtime.seeded.js +70 -0
  11. package/dist/typeConverters/typeConverters.core.d.ts +38 -3
  12. package/dist/typeConverters/typeConverters.core.js +111 -20
  13. package/dist/typeGuards/index.d.ts +1 -1
  14. package/dist/typeGuards/index.js +1 -1
  15. package/dist/typeGuards/typeGuards.core.d.ts +53 -4
  16. package/dist/typeGuards/typeGuards.core.js +112 -13
  17. package/dist/typeUtilities/index.d.ts +1 -1
  18. package/dist/typeUtilities/typeUtilities.core.d.ts +22 -3
  19. package/package.json +23 -12
  20. package/dist/.tsbuildinfo +0 -1
  21. package/dist/index.d.ts.map +0 -1
  22. package/dist/index.js.map +0 -1
  23. package/dist/runtime/index.d.ts.map +0 -1
  24. package/dist/runtime/index.js.map +0 -1
  25. package/dist/runtime/runtime.core.d.ts.map +0 -1
  26. package/dist/runtime/runtime.core.js.map +0 -1
  27. package/dist/typeConverters/index.d.ts.map +0 -1
  28. package/dist/typeConverters/index.js.map +0 -1
  29. package/dist/typeConverters/typeConverters.core.d.ts.map +0 -1
  30. package/dist/typeConverters/typeConverters.core.js.map +0 -1
  31. package/dist/typeGuards/index.d.ts.map +0 -1
  32. package/dist/typeGuards/index.js.map +0 -1
  33. package/dist/typeGuards/typeGuards.core.d.ts.map +0 -1
  34. package/dist/typeGuards/typeGuards.core.js.map +0 -1
  35. package/dist/typeUtilities/index.d.ts.map +0 -1
  36. package/dist/typeUtilities/index.js.map +0 -1
  37. package/dist/typeUtilities/typeUtilities.core.d.ts.map +0 -1
  38. package/dist/typeUtilities/typeUtilities.core.js.map +0 -1
@@ -3,12 +3,35 @@
3
3
  *
4
4
  * @module typeConverters/typeConverters
5
5
  */
6
+ /** Property names that mutate a prototype instead of adding a key. */
7
+ const UNSAFE_KEYS = new Set([
8
+ "__proto__",
9
+ "constructor",
10
+ "prototype",
11
+ ]);
6
12
  /**
7
13
  * Safely parse JSON with a fallback value.
14
+ *
15
+ * "Safe" here means only that malformed JSON yields the fallback rather than
16
+ * throwing. The result is cast to `T` without validation — parse a trust
17
+ * boundary with `@zudojs/validation` or `@zudojs/schema` instead of relying on
18
+ * this cast.
19
+ *
20
+ * `__proto__`, `constructor` and `prototype` keys are dropped at every depth.
21
+ * `JSON.parse` itself never routes them through a prototype, so this is a
22
+ * deliberate deny-list, not a parser fix: a downstream deep merge that walks
23
+ * `constructor.prototype` or `__proto__` would otherwise reach
24
+ * `Object.prototype`. A payload whose legitimate field is named
25
+ * `constructor` or `prototype` loses that field; parse it with plain
26
+ * `JSON.parse` and validate it instead.
8
27
  */
9
28
  export function safeJsonParse(json, fallback) {
10
29
  try {
11
- return JSON.parse(json);
30
+ return JSON.parse(json, function reviver(key, value) {
31
+ if (UNSAFE_KEYS.has(key))
32
+ return undefined;
33
+ return value;
34
+ });
12
35
  }
13
36
  catch {
14
37
  return fallback;
@@ -16,48 +39,81 @@ export function safeJsonParse(json, fallback) {
16
39
  }
17
40
  /**
18
41
  * Convert a value to a string safely.
42
+ *
43
+ * Always returns a string. `JSON.stringify` returns the *value* `undefined`
44
+ * — not a string, and without throwing — for functions, symbols and
45
+ * `undefined`, so its result is checked rather than returned directly.
19
46
  */
20
47
  export function toString(value, fallback = "") {
21
48
  if (value === null || value === undefined)
22
49
  return fallback;
23
50
  if (typeof value === "string")
24
51
  return value;
25
- if (typeof value === "number" || typeof value === "boolean")
52
+ if (typeof value === "number" || typeof value === "boolean") {
26
53
  return String(value);
54
+ }
55
+ if (typeof value === "bigint")
56
+ return `${value}`;
57
+ if (typeof value === "symbol")
58
+ return value.toString();
59
+ if (typeof value === "function")
60
+ return fallback;
27
61
  try {
28
- return JSON.stringify(value);
62
+ const serialized = JSON.stringify(value);
63
+ return typeof serialized === "string" ? serialized : fallback;
29
64
  }
30
65
  catch {
31
66
  return fallback;
32
67
  }
33
68
  }
34
69
  /**
35
- * Convert a value to a number safely.
70
+ * Convert a value to a finite number safely.
71
+ *
72
+ * Blank strings, hexadecimal literals and infinities all fall back rather than
73
+ * converting: a missing query parameter arriving as `""` becoming a real zero
74
+ * silently turns into a page size, a price or a limit.
36
75
  */
37
76
  export function toNumber(value, fallback = NaN) {
38
- if (typeof value === "number")
39
- return value;
77
+ if (typeof value === "number") {
78
+ return Number.isFinite(value) ? value : fallback;
79
+ }
40
80
  if (typeof value === "string") {
41
- const num = Number(value);
42
- return Number.isNaN(num) ? fallback : num;
81
+ const trimmed = value.trim();
82
+ if (trimmed.length === 0)
83
+ return fallback;
84
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(trimmed)) {
85
+ return fallback;
86
+ }
87
+ const parsed = Number(trimmed);
88
+ return Number.isFinite(parsed) ? parsed : fallback;
43
89
  }
44
90
  return fallback;
45
91
  }
46
92
  /**
47
93
  * Convert a value to a boolean safely.
94
+ *
95
+ * `NaN` falls back rather than converting to true. It is what `toNumber`
96
+ * produces on failure, so chaining the two would otherwise turn a parse
97
+ * failure into the permissive answer for a flag.
48
98
  */
49
99
  export function toBoolean(value, fallback = false) {
50
100
  if (typeof value === "boolean")
51
101
  return value;
52
102
  if (typeof value === "string") {
53
103
  const lower = value.toLowerCase().trim();
54
- if (lower === "true" || lower === "1" || lower === "yes")
104
+ if (lower === "true" || lower === "1" || lower === "yes" || lower === "on")
55
105
  return true;
56
- if (lower === "false" || lower === "0" || lower === "no" || lower === "")
106
+ if (lower === "false" ||
107
+ lower === "0" ||
108
+ lower === "no" ||
109
+ lower === "off" ||
110
+ lower === "")
57
111
  return false;
112
+ return fallback;
113
+ }
114
+ if (typeof value === "number") {
115
+ return Number.isNaN(value) ? fallback : value !== 0;
58
116
  }
59
- if (typeof value === "number")
60
- return value !== 0;
61
117
  return fallback;
62
118
  }
63
119
  /**
@@ -70,11 +126,22 @@ export function toArray(value) {
70
126
  }
71
127
  /**
72
128
  * Convert a Map to a plain object.
129
+ *
130
+ * Built on a null-prototype object with `defineProperty`. Assigning into an
131
+ * object literal routes a `__proto__` key through the prototype setter, so a
132
+ * Map built from request data — headers, form fields, query parameters — could
133
+ * replace the result's prototype with attacker-supplied values that
134
+ * `Object.keys` does not reveal.
73
135
  */
74
136
  export function mapToObject(map) {
75
- const obj = {};
137
+ const obj = Object.create(null);
76
138
  for (const [key, value] of map) {
77
- obj[key] = value;
139
+ Object.defineProperty(obj, key, {
140
+ value,
141
+ enumerable: true,
142
+ writable: true,
143
+ configurable: true,
144
+ });
78
145
  }
79
146
  return obj;
80
147
  }
@@ -88,24 +155,48 @@ export function objectToMap(obj) {
88
155
  * Convert snake_case to camelCase.
89
156
  */
90
157
  export function snakeToCamel(str) {
91
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
158
+ return str.replace(/(?<!_)_+([a-z0-9])/gu, (_, char) => char.toUpperCase());
159
+ }
160
+ /**
161
+ * Split a camelCase or PascalCase identifier into its words.
162
+ *
163
+ * Runs of capitals are kept together, so `parseHTTPResponse` yields
164
+ * `["parse", "HTTP", "Response"]` rather than one word per letter. Letter
165
+ * classes are Unicode-aware (`caféAuLait` keeps its `é`), existing `_`, `-`
166
+ * and whitespace separators are word boundaries, and any other character is
167
+ * kept in place rather than silently dropped.
168
+ */
169
+ function splitCamelWords(str) {
170
+ return str
171
+ .replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1\u0000$2")
172
+ .replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, "$1\u0000$2")
173
+ .split(/[\u0000\s_-]+/u)
174
+ .filter((word) => word.length > 0);
92
175
  }
93
176
  /**
94
- * Convert camelCase to snake_case.
177
+ * Convert camelCase or PascalCase to snake_case.
178
+ *
179
+ * Leading capitals do not produce a leading separator, and acronyms survive
180
+ * as single words — an identifier like `_hello_world` is not a valid column
181
+ * name, and `parse_h_t_t_p_response` is not a useful one.
95
182
  */
96
183
  export function camelToSnake(str) {
97
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
184
+ return splitCamelWords(str)
185
+ .map((word) => word.toLowerCase())
186
+ .join("_");
98
187
  }
99
188
  /**
100
189
  * Convert kebab-case to camelCase.
101
190
  */
102
191
  export function kebabToCamel(str) {
103
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
192
+ return str.replace(/(?<!-)-+([a-z0-9])/gu, (_, char) => char.toUpperCase());
104
193
  }
105
194
  /**
106
- * Convert camelCase to kebab-case.
195
+ * Convert camelCase or PascalCase to kebab-case.
107
196
  */
108
197
  export function camelToKebab(str) {
109
- return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
198
+ return splitCamelWords(str)
199
+ .map((word) => word.toLowerCase())
200
+ .join("-");
110
201
  }
111
202
  //# sourceMappingURL=typeConverters.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module typeGuards
5
5
  */
6
- export { isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isInteger, isDate, isUrl, isEmail, isUuid, isIsoDateString, isArrayOfType, isDefined, isFunction, isPromise, } from "./typeGuards.core.js";
6
+ export { MAX_EMAIL_LENGTH, isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isFiniteNumber, isInteger, isDate, isUrl, isEmail, isUuid, isUuidV4, isIsoDateString, isIsoDateTimeString, isArrayOfType, isDefined, isFunction, isPromise, isThenable, } from "./typeGuards.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module typeGuards
5
5
  */
6
- export { isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isInteger, isDate, isUrl, isEmail, isUuid, isIsoDateString, isArrayOfType, isDefined, isFunction, isPromise, } from "./typeGuards.core.js";
6
+ export { MAX_EMAIL_LENGTH, isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isFiniteNumber, isInteger, isDate, isUrl, isEmail, isUuid, isUuidV4, isIsoDateString, isIsoDateTimeString, isArrayOfType, isDefined, isFunction, isPromise, isThenable, } from "./typeGuards.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -16,7 +16,14 @@ export declare function isNonNullObject(value: unknown): value is Record<string,
16
16
  */
17
17
  export declare function isNonEmptyString(value: unknown): value is string;
18
18
  /**
19
- * Check if a value is a positive number.
19
+ * Check if a value is a finite number.
20
+ */
21
+ export declare function isFiniteNumber(value: unknown): value is number;
22
+ /**
23
+ * Check if a value is a positive, finite number.
24
+ *
25
+ * `Infinity` is excluded: a positive-number guard is normally protecting a
26
+ * size, a count or a price, none of which have a meaningful infinite value.
20
27
  */
21
28
  export declare function isPositiveNumber(value: unknown): value is number;
22
29
  /**
@@ -31,18 +38,52 @@ export declare function isDate(value: unknown): value is Date;
31
38
  * Check if a value is a valid URL string.
32
39
  */
33
40
  export declare function isUrl(value: unknown): value is string;
41
+ /**
42
+ * Longest input `isEmail` will examine.
43
+ *
44
+ * RFC 5321 caps a forward path at 254 characters; anything longer cannot be a
45
+ * deliverable address, and refusing it up front keeps an attacker from
46
+ * handing the domain pattern a megabyte to backtrack over.
47
+ */
48
+ export declare const MAX_EMAIL_LENGTH = 254;
34
49
  /**
35
50
  * Check if a value is a valid email string.
51
+ *
52
+ * This is the monorepo's reference email check. `ValidationPattern.EMAIL`
53
+ * in `@zudojs/constants` (and therefore `createEmailAddress` and the schema
54
+ * `email` format) encodes the same acceptance set, including the 254
55
+ * character bound. `@zudojs/validation`'s `email` constraint still carries
56
+ * its own copy of the pattern without the length bound; until it delegates
57
+ * here, an address over 254 characters passes that constraint only.
36
58
  */
37
59
  export declare function isEmail(value: unknown): value is string;
38
60
  /**
39
- * Check if a value is a valid UUID v4 string.
61
+ * Check if a value is a UUID string of any defined version.
62
+ *
63
+ * Accepts versions 1 through 8 — UUIDv7 included — plus the nil and max
64
+ * UUIDs. Use {@link isUuidV4} when the version genuinely matters.
40
65
  */
41
66
  export declare function isUuid(value: unknown): value is string;
42
67
  /**
43
- * Check if a value is a valid ISO 8601 date string.
68
+ * Check if a value is specifically a UUID v4 string.
69
+ */
70
+ export declare function isUuidV4(value: unknown): value is string;
71
+ /**
72
+ * Check if a value is a valid ISO 8601 date string, with or without a time.
73
+ *
74
+ * Validates the calendar date as well as the shape, and accepts numeric UTC
75
+ * offsets. Checking digit counts alone accepted `2024-13-45T99:99:99Z` and
76
+ * rejected `2024-01-01T00:00:00+02:00` — wrong in both directions.
77
+ *
78
+ * Use {@link isIsoDateTimeString} where a time component is required.
44
79
  */
45
80
  export declare function isIsoDateString(value: unknown): value is string;
81
+ /**
82
+ * Check if a value is a valid ISO 8601 date-time string.
83
+ *
84
+ * Like {@link isIsoDateString}, but a time component is mandatory.
85
+ */
86
+ export declare function isIsoDateTimeString(value: unknown): value is string;
46
87
  /**
47
88
  * Check if a value is an array of a specific element type.
48
89
  */
@@ -56,7 +97,15 @@ export declare function isDefined<T>(value: T | null | undefined): value is T;
56
97
  */
57
98
  export declare function isFunction(value: unknown): value is (...args: unknown[]) => unknown;
58
99
  /**
59
- * Check if a value is a Promise.
100
+ * Check if a value is a native Promise.
60
101
  */
61
102
  export declare function isPromise(value: unknown): value is Promise<unknown>;
103
+ /**
104
+ * Check if a value is awaitable.
105
+ *
106
+ * Narrows to `PromiseLike`, not `Promise`: a plain thenable is safe to
107
+ * `await` but has no `.catch()` or `.finally()`, so claiming it is a Promise
108
+ * makes those calls throw at the point the guard was supposed to make safe.
109
+ */
110
+ export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
62
111
  //# sourceMappingURL=typeGuards.core.d.ts.map
@@ -25,10 +25,19 @@ export function isNonEmptyString(value) {
25
25
  return typeof value === "string" && value.length > 0;
26
26
  }
27
27
  /**
28
- * Check if a value is a positive number.
28
+ * Check if a value is a finite number.
29
+ */
30
+ export function isFiniteNumber(value) {
31
+ return typeof value === "number" && Number.isFinite(value);
32
+ }
33
+ /**
34
+ * Check if a value is a positive, finite number.
35
+ *
36
+ * `Infinity` is excluded: a positive-number guard is normally protecting a
37
+ * size, a count or a price, none of which have a meaningful infinite value.
29
38
  */
30
39
  export function isPositiveNumber(value) {
31
- return typeof value === "number" && value > 0 && !Number.isNaN(value);
40
+ return isFiniteNumber(value) && value > 0;
32
41
  }
33
42
  /**
34
43
  * Check if a value is a valid integer.
@@ -56,29 +65,108 @@ export function isUrl(value) {
56
65
  return false;
57
66
  }
58
67
  }
68
+ /**
69
+ * Longest input `isEmail` will examine.
70
+ *
71
+ * RFC 5321 caps a forward path at 254 characters; anything longer cannot be a
72
+ * deliverable address, and refusing it up front keeps an attacker from
73
+ * handing the domain pattern a megabyte to backtrack over.
74
+ */
75
+ export const MAX_EMAIL_LENGTH = 254;
59
76
  /**
60
77
  * Check if a value is a valid email string.
78
+ *
79
+ * This is the monorepo's reference email check. `ValidationPattern.EMAIL`
80
+ * in `@zudojs/constants` (and therefore `createEmailAddress` and the schema
81
+ * `email` format) encodes the same acceptance set, including the 254
82
+ * character bound. `@zudojs/validation`'s `email` constraint still carries
83
+ * its own copy of the pattern without the length bound; until it delegates
84
+ * here, an address over 254 characters passes that constraint only.
61
85
  */
62
86
  export function isEmail(value) {
63
87
  if (typeof value !== "string")
64
88
  return false;
65
- return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
89
+ // Bound the input before the pattern runs against it. The domain half
90
+ // contains `(?:[a-z0-9-]*[a-z0-9])?` nested inside `(?:\. … )+`, which is
91
+ // the ambiguous-quantifier shape that backtracks super-linearly on a long
92
+ // non-matching label. RFC 5321 caps a path at 254 characters, so nothing
93
+ // legitimate is lost by refusing to even look at more.
94
+ if (value.length > MAX_EMAIL_LENGTH)
95
+ return false;
96
+ if (value.includes(".."))
97
+ return false;
98
+ return /^[^\s@,;<>"[\]\\]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/iu.test(value);
66
99
  }
67
100
  /**
68
- * Check if a value is a valid UUID v4 string.
101
+ * Check if a value is a UUID string of any defined version.
102
+ *
103
+ * Accepts versions 1 through 8 — UUIDv7 included — plus the nil and max
104
+ * UUIDs. Use {@link isUuidV4} when the version genuinely matters.
69
105
  */
70
106
  export function isUuid(value) {
71
107
  if (typeof value !== "string")
72
108
  return false;
73
- return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
109
+ if (value === "00000000-0000-0000-0000-000000000000")
110
+ return true;
111
+ if (value.toLowerCase() === "ffffffff-ffff-ffff-ffff-ffffffffffff") {
112
+ return true;
113
+ }
114
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value);
74
115
  }
75
116
  /**
76
- * Check if a value is a valid ISO 8601 date string.
117
+ * Check if a value is specifically a UUID v4 string.
118
+ */
119
+ export function isUuidV4(value) {
120
+ if (typeof value !== "string")
121
+ return false;
122
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value);
123
+ }
124
+ /**
125
+ * Check if a value is a valid ISO 8601 date string, with or without a time.
126
+ *
127
+ * Validates the calendar date as well as the shape, and accepts numeric UTC
128
+ * offsets. Checking digit counts alone accepted `2024-13-45T99:99:99Z` and
129
+ * rejected `2024-01-01T00:00:00+02:00` — wrong in both directions.
130
+ *
131
+ * Use {@link isIsoDateTimeString} where a time component is required.
77
132
  */
78
133
  export function isIsoDateString(value) {
79
134
  if (typeof value !== "string")
80
135
  return false;
81
- return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/.test(value);
136
+ if (!/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})?)?$/u.test(value)) {
137
+ return false;
138
+ }
139
+ if (Number.isNaN(new Date(value).getTime()))
140
+ return false;
141
+ // Real guards rather than `!` assertions: the regex above already fixes the
142
+ // shape, but an assertion would turn any future loosening of it into silent
143
+ // NaN propagation through Date.UTC instead of a `false`.
144
+ const parts = value.slice(0, 10).split("-").map(Number);
145
+ const [year, month, day] = parts;
146
+ if (year === undefined || month === undefined || day === undefined) {
147
+ return false;
148
+ }
149
+ if (!Number.isInteger(year) ||
150
+ !Number.isInteger(month) ||
151
+ !Number.isInteger(day)) {
152
+ return false;
153
+ }
154
+ const asUtc = new Date(Date.UTC(year, month - 1, day));
155
+ return (asUtc.getUTCFullYear() === year &&
156
+ asUtc.getUTCMonth() === month - 1 &&
157
+ asUtc.getUTCDate() === day);
158
+ }
159
+ /**
160
+ * Check if a value is a valid ISO 8601 date-time string.
161
+ *
162
+ * Like {@link isIsoDateString}, but a time component is mandatory.
163
+ */
164
+ export function isIsoDateTimeString(value) {
165
+ if (typeof value !== "string")
166
+ return false;
167
+ if (!/[T ]\d{2}:\d{2}/u.test(value))
168
+ return false;
169
+ return isIsoDateString(value);
82
170
  }
83
171
  /**
84
172
  * Check if a value is an array of a specific element type.
@@ -101,13 +189,24 @@ export function isFunction(value) {
101
189
  return typeof value === "function";
102
190
  }
103
191
  /**
104
- * Check if a value is a Promise.
192
+ * Check if a value is a native Promise.
105
193
  */
106
194
  export function isPromise(value) {
107
- return (value instanceof Promise ||
108
- (typeof value === "object" &&
109
- value !== null &&
110
- "then" in value &&
111
- typeof value.then === "function"));
195
+ return value instanceof Promise;
196
+ }
197
+ /**
198
+ * Check if a value is awaitable.
199
+ *
200
+ * Narrows to `PromiseLike`, not `Promise`: a plain thenable is safe to
201
+ * `await` but has no `.catch()` or `.finally()`, so claiming it is a Promise
202
+ * makes those calls throw at the point the guard was supposed to make safe.
203
+ */
204
+ export function isThenable(value) {
205
+ if (value instanceof Promise)
206
+ return true;
207
+ return (typeof value === "object" &&
208
+ value !== null &&
209
+ "then" in value &&
210
+ typeof value.then === "function");
112
211
  }
113
212
  //# sourceMappingURL=typeGuards.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module typeUtilities
5
5
  */
6
- export type { DeepReadonly, DeepPartial, DeepRequired, Prettify, StringKeysOf, NumberKeysOf, PartialExcept, RequiredExcept, OptionalKeys, RequireKeys, AsyncReturnType, Nullable, Undefinable, Maybe, MaybePromise, NestedKeyOf, NestedValueOf, OmitByValue, PickByValue, } from "./typeUtilities.core.js";
6
+ export type { DeepReadonly, DeepPartial, DeepRequired, Prettify, StringKeysOf, NumberKeysOf, PartialExcept, RequiredExcept, PartialKeys, OptionalKeyNames, RequireKeys, AsyncReturnType, Nullable, Undefinable, Maybe, MaybePromise, NestedKeyOf, NestedValueOf, OmitByValue, PickByValue, } from "./typeUtilities.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -44,17 +44,36 @@ export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T,
44
44
  */
45
45
  export type RequiredExcept<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>;
46
46
  /**
47
- * Create a type that makes specified keys optional.
47
+ * Create a type that makes the specified keys optional.
48
+ *
49
+ * Named `PartialKeys` to pair with {@link RequireKeys}. It was `OptionalKeys`,
50
+ * which in every other utility library means "the union of `T`'s optional key
51
+ * names" — the opposite kind of thing, and a two-parameter type that reads as
52
+ * a one-parameter one. Use {@link OptionalKeyNames} for that meaning.
48
53
  */
49
- export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
54
+ export type PartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
55
+ /**
56
+ * The union of `T`'s optional key names.
57
+ *
58
+ * This is what `OptionalKeys<T>` reads as, and there was no type that did it.
59
+ */
60
+ export type OptionalKeyNames<T> = {
61
+ [K in keyof T]-?: object extends Pick<T, K> ? K : never;
62
+ }[keyof T];
50
63
  /**
51
64
  * Create a type that makes specified keys required.
52
65
  */
53
66
  export type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
54
67
  /**
55
68
  * Extract the return type of an async function.
69
+ *
70
+ * The constraint uses `never[]` rather than `unknown[]`. Parameters are
71
+ * contravariant, so `(id: string) => Promise<User>` is **not** assignable to
72
+ * `(...args: unknown[]) => Promise<unknown>` — the previous constraint
73
+ * rejected almost every real async function, and nothing caught it because no
74
+ * test instantiated the type.
56
75
  */
57
- export type AsyncReturnType<T extends (...args: unknown[]) => Promise<unknown>> = T extends (...args: unknown[]) => Promise<infer R> ? R : never;
76
+ export type AsyncReturnType<T extends (...args: never[]) => Promise<unknown>> = T extends (...args: never[]) => Promise<infer R> ? R : never;
58
77
  /**
59
78
  * Make a type nullable (allows null).
60
79
  */
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/types",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared type guards, utility types, and type converters for the Zudojs framework.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -15,22 +19,18 @@
15
19
  }
16
20
  },
17
21
  "files": [
18
- "dist"
22
+ "dist",
23
+ "!dist/**/*.map",
24
+ "!dist/**/*.tsbuildinfo",
25
+ "!dist/.tsbuildinfo"
19
26
  ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "typecheck": "tsc -p tsconfig.json --noEmit",
23
- "clean": "rm -rf dist",
24
- "test": "vitest run",
25
- "test:watch": "vitest"
26
- },
27
27
  "dependencies": {},
28
28
  "engines": {
29
29
  "node": ">=24.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.4.1",
33
- "typescript": "^7.0.2",
33
+ "typescript": "7.0.2",
34
34
  "vitest": "^4.1.11"
35
35
  },
36
36
  "publishConfig": {
@@ -43,8 +43,19 @@
43
43
  "utilities"
44
44
  ],
45
45
  "homepage": "https://github.com/oyinlola-tech/zudo#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/oyinlola-tech/zudo/issues"
48
+ },
46
49
  "repository": {
47
50
  "type": "git",
48
- "url": "https://github.com/oyinlola-tech/zudo"
51
+ "url": "https://github.com/oyinlola-tech/zudo",
52
+ "directory": "packages/types"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.json",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
57
+ "clean": "rm -rf dist",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
49
60
  }
50
- }
61
+ }