ts-data-forge 14.0.1 → 14.2.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 +5 -5
- package/dist/array/impl/array-utils-transformation.d.mts +4 -1
- package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-transformation.mjs +4 -5
- package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
- package/dist/array/impl/array-utils-validation.d.mts +8 -3
- package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-validation.mjs.map +1 -1
- package/dist/collections/imap-mapped.mjs +1 -1
- package/dist/collections/imap-mapped.mjs.map +1 -1
- package/dist/collections/imap.mjs +1 -1
- package/dist/collections/imap.mjs.map +1 -1
- package/dist/functional/match.d.mts.map +1 -1
- package/dist/functional/match.mjs.map +1 -1
- package/dist/functional/optional/impl/optional-map.d.mts +1 -1
- package/dist/functional/optional/impl/optional-map.d.mts.map +1 -1
- package/dist/functional/optional/impl/optional-map.mjs.map +1 -1
- package/dist/functional/result/impl/result-map-err.d.mts +1 -1
- package/dist/functional/result/impl/result-map-err.d.mts.map +1 -1
- package/dist/functional/result/impl/result-map-err.mjs.map +1 -1
- package/dist/functional/result/impl/result-map.d.mts +1 -1
- package/dist/functional/result/impl/result-map.d.mts.map +1 -1
- package/dist/functional/result/impl/result-map.mjs.map +1 -1
- package/dist/number/branded-types/int.d.mts +1 -1
- package/dist/number/branded-types/int16.d.mts +1 -1
- package/dist/number/branded-types/int32.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
- package/dist/number/branded-types/non-positive-finite-number.mjs +1 -1
- package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -1
- package/dist/number/branded-types/positive-int.d.mts +1 -1
- package/dist/number/branded-types/positive-int16.d.mts +1 -1
- package/dist/number/branded-types/positive-int32.d.mts +1 -1
- package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
- package/dist/number/branded-types/positive-uint16.d.mts +1 -1
- package/dist/number/branded-types/positive-uint32.d.mts +1 -1
- package/dist/number/branded-types/safe-int.d.mts +1 -1
- package/dist/number/branded-types/safe-uint.d.mts +1 -1
- package/dist/number/branded-types/uint.d.mts +1 -1
- package/dist/number/branded-types/uint16.d.mts +1 -1
- package/dist/number/branded-types/uint32.d.mts +1 -1
- package/dist/number/enum/int8.d.mts +2 -2
- package/dist/number/enum/int8.d.mts.map +1 -1
- package/dist/number/enum/int8.mjs.map +1 -1
- package/dist/number/enum/uint8.d.mts +2 -2
- package/dist/number/enum/uint8.d.mts.map +1 -1
- package/dist/number/enum/uint8.mjs.map +1 -1
- package/dist/number/num-arithmetic-types.d.mts +8 -9
- package/dist/number/num-arithmetic-types.d.mts.map +1 -1
- package/dist/number/refined-number-utils.d.mts +2 -2
- package/dist/number/refined-number-utils.d.mts.map +1 -1
- package/dist/number/refined-number-utils.mjs.map +1 -1
- package/dist/object/object.d.mts +12 -12
- package/dist/object/object.d.mts.map +1 -1
- package/dist/object/object.mjs +5 -1
- package/dist/object/object.mjs.map +1 -1
- package/dist/others/fast-deep-equal.d.mts.map +1 -1
- package/dist/others/fast-deep-equal.mjs +2 -2
- package/dist/others/fast-deep-equal.mjs.map +1 -1
- package/package.json +2 -2
- package/src/array/impl/array-utils-modification.test.mts +26 -5
- package/src/array/impl/array-utils-shape-invariants.test.mts +259 -17
- package/src/array/impl/array-utils-slicing.test.mts +11 -3
- package/src/array/impl/array-utils-transformation.mts +41 -18
- package/src/array/impl/array-utils-transformation.test.mts +10 -3
- package/src/array/impl/array-utils-validation.mts +54 -10
- package/src/array/impl/array-utils-validation.test.mts +153 -3
- package/src/collections/imap-mapped.mts +1 -1
- package/src/collections/imap-mapped.test.mts +2 -2
- package/src/collections/imap.mts +1 -1
- package/src/collections/iset-mapped.test.mts +1 -1
- package/src/functional/match.mts +5 -4
- package/src/functional/optional/impl/optional-map.mts +16 -1
- package/src/functional/optional.test.mts +48 -1
- package/src/functional/result/impl/result-map-err.mts +16 -1
- package/src/functional/result/impl/result-map.mts +16 -1
- package/src/functional/result.test.mts +83 -3
- package/src/guard/is-primitive.test.mts +1 -1
- package/src/guard/is-record.test.mts +4 -4
- package/src/guard/is-type.test.mts +2 -2
- package/src/number/branded-types/finite-number.test.mts +2 -2
- package/src/number/branded-types/non-negative-finite-number.test.mts +2 -2
- package/src/number/branded-types/non-positive-finite-number.mts +1 -1
- package/src/number/branded-types/non-positive-finite-number.test.mts +5 -9
- package/src/number/branded-types/non-zero-finite-number.test.mts +2 -2
- package/src/number/branded-types/positive-finite-number.test.mts +2 -2
- package/src/number/enum/int8.mts +2 -1
- package/src/number/enum/uint8.mts +6 -2
- package/src/number/num-arithmetic-types.mts +125 -24
- package/src/number/num.test.mts +9 -9
- package/src/number/refined-number-utils.mts +2 -1
- package/src/object/object.mts +29 -17
- package/src/object/object.test.mts +2 -1
- package/src/others/fast-deep-equal.mts +6 -4
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BoundedLengthArray,
|
|
3
|
+
type FixedLengthArray,
|
|
4
|
+
type FixedLengthTuple,
|
|
5
|
+
type MinLengthArray,
|
|
6
|
+
type ReadonlyRecord,
|
|
7
|
+
} from 'ts-type-forge';
|
|
1
8
|
import { expectType } from '../../expect-type.mjs';
|
|
9
|
+
import {
|
|
10
|
+
asBoundedLengthArray,
|
|
11
|
+
asFixedLengthArray,
|
|
12
|
+
asMinLengthArray,
|
|
13
|
+
} from './array-utils-length-bounded-array-cast.mjs';
|
|
2
14
|
import {
|
|
3
15
|
every,
|
|
4
16
|
indexIsInRange,
|
|
@@ -294,13 +306,12 @@ describe('Arr validations', () => {
|
|
|
294
306
|
type ComplexUnion =
|
|
295
307
|
| Readonly<
|
|
296
308
|
| { type: 'array'; data: readonly string[] }
|
|
297
|
-
| { type: 'object'; data:
|
|
309
|
+
| { type: 'object'; data: ReadonlyRecord<string, unknown> }
|
|
298
310
|
>
|
|
299
311
|
| readonly number[]
|
|
300
312
|
| string
|
|
301
313
|
| null;
|
|
302
314
|
|
|
303
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
304
315
|
const processComplex = (value: ComplexUnion): number => {
|
|
305
316
|
if (isArray(value)) {
|
|
306
317
|
expectType<typeof value, readonly number[]>('=');
|
|
@@ -324,7 +335,7 @@ describe('Arr validations', () => {
|
|
|
324
335
|
typeof value,
|
|
325
336
|
Readonly<
|
|
326
337
|
| { type: 'array'; data: readonly string[] }
|
|
327
|
-
| { type: 'object'; data:
|
|
338
|
+
| { type: 'object'; data: ReadonlyRecord<string, unknown> }
|
|
328
339
|
>
|
|
329
340
|
>('=');
|
|
330
341
|
|
|
@@ -436,6 +447,145 @@ describe('Arr validations', () => {
|
|
|
436
447
|
|
|
437
448
|
assert.isTrue(indexMatchesValue);
|
|
438
449
|
});
|
|
450
|
+
|
|
451
|
+
/* What `every` reports about the array it narrowed, across the shapes the
|
|
452
|
+
two type-guard overloads have to cope with. It used to answer a bare
|
|
453
|
+
`readonly S[]` for every one of them, dropping both the tuple length and
|
|
454
|
+
the length-constraint brand. `assert.isTrue` narrows for the rest of the
|
|
455
|
+
block, so each case checks the runtime answer and the narrowed type
|
|
456
|
+
together. */
|
|
457
|
+
describe('narrowing', () => {
|
|
458
|
+
const isString = (x: string | number): x is string =>
|
|
459
|
+
typeof x === 'string';
|
|
460
|
+
|
|
461
|
+
const values: readonly (string | number)[] = [
|
|
462
|
+
'Ada',
|
|
463
|
+
'Grace',
|
|
464
|
+
'Katherine',
|
|
465
|
+
] as const;
|
|
466
|
+
|
|
467
|
+
test('should keep a plain array as a plain array', () => {
|
|
468
|
+
const shapeArray: readonly (string | number)[] = values;
|
|
469
|
+
|
|
470
|
+
assert.isTrue(every(shapeArray, isString));
|
|
471
|
+
|
|
472
|
+
expectType<typeof shapeArray, readonly string[]>('~=');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('should keep the length of a tuple', () => {
|
|
476
|
+
const shapeTuple: FixedLengthTuple<2, string | number> = [
|
|
477
|
+
'Ada',
|
|
478
|
+
'Grace',
|
|
479
|
+
] as const;
|
|
480
|
+
|
|
481
|
+
assert.isTrue(every(shapeTuple, isString));
|
|
482
|
+
|
|
483
|
+
expectType<typeof shapeTuple, FixedLengthTuple<2, string>>('~=');
|
|
484
|
+
|
|
485
|
+
expectType<(typeof shapeTuple)['length'], 2>('=');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('should keep a MinLengthArray brand', () => {
|
|
489
|
+
const shapeBranded = asMinLengthArray(3, values);
|
|
490
|
+
|
|
491
|
+
assert.isTrue(every(shapeBranded, isString));
|
|
492
|
+
|
|
493
|
+
expectType<typeof shapeBranded, MinLengthArray<3, string>>('~=');
|
|
494
|
+
|
|
495
|
+
// The brand still rules out `undefined` below the minimum length.
|
|
496
|
+
expectType<(typeof shapeBranded)[0], string>('=');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('should keep a BoundedLengthArray brand', () => {
|
|
500
|
+
const shapeBounded = asBoundedLengthArray(2, 5, values);
|
|
501
|
+
|
|
502
|
+
assert.isTrue(every(shapeBounded, isString));
|
|
503
|
+
|
|
504
|
+
expectType<typeof shapeBounded, BoundedLengthArray<2, 5, string>>('~=');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('should keep a FixedLengthArray brand', () => {
|
|
508
|
+
const shapeFixed = asFixedLengthArray(3, values);
|
|
509
|
+
|
|
510
|
+
assert.isTrue(every(shapeFixed, isString));
|
|
511
|
+
|
|
512
|
+
expectType<typeof shapeFixed, FixedLengthArray<3, string>>('~=');
|
|
513
|
+
|
|
514
|
+
expectType<(typeof shapeFixed)['length'], 3>('=');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
/* Brand intersected with an exact tuple — the shape the `as*` casts and
|
|
518
|
+
the `is*` guards produce. Brand, length and positions all survive. */
|
|
519
|
+
test('should keep brand, length and positions of a branded tuple', () => {
|
|
520
|
+
const tuple: readonly [string, string | number] = [
|
|
521
|
+
'Ada',
|
|
522
|
+
'Grace',
|
|
523
|
+
] as const;
|
|
524
|
+
|
|
525
|
+
const shapeBrandedTuple = asMinLengthArray(2, tuple);
|
|
526
|
+
|
|
527
|
+
assert.isTrue(every(shapeBrandedTuple, isString));
|
|
528
|
+
|
|
529
|
+
expectType<(typeof shapeBrandedTuple)[0], string>('=');
|
|
530
|
+
|
|
531
|
+
expectType<(typeof shapeBrandedTuple)[1], string>('=');
|
|
532
|
+
|
|
533
|
+
expectType<(typeof shapeBrandedTuple)['length'], 2>('=');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
/* The curried form has to reach the same conclusions. Its two cases are
|
|
537
|
+
overloads of the *returned* guard rather than of `every` itself, so a
|
|
538
|
+
single `every(predicate)` value serves branded and unbranded arrays
|
|
539
|
+
alike — spelled as two overloads of `every`, the branded one shadowed
|
|
540
|
+
the other and passing a plain array to the result was an error. */
|
|
541
|
+
describe('curried', () => {
|
|
542
|
+
const allStrings = every(isString);
|
|
543
|
+
|
|
544
|
+
test('should keep a plain array as a plain array', () => {
|
|
545
|
+
const shapeArray: readonly (string | number)[] = values;
|
|
546
|
+
|
|
547
|
+
assert.isTrue(allStrings(shapeArray));
|
|
548
|
+
|
|
549
|
+
expectType<typeof shapeArray, readonly string[]>('~=');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('should keep the length of a tuple', () => {
|
|
553
|
+
const shapeTuple: FixedLengthTuple<2, string | number> = [
|
|
554
|
+
'Ada',
|
|
555
|
+
'Grace',
|
|
556
|
+
] as const;
|
|
557
|
+
|
|
558
|
+
assert.isTrue(allStrings(shapeTuple));
|
|
559
|
+
|
|
560
|
+
expectType<typeof shapeTuple, FixedLengthTuple<2, string>>('~=');
|
|
561
|
+
|
|
562
|
+
expectType<(typeof shapeTuple)['length'], 2>('=');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('should keep a MinLengthArray brand', () => {
|
|
566
|
+
const shapeBranded = asMinLengthArray(3, values);
|
|
567
|
+
|
|
568
|
+
assert.isTrue(allStrings(shapeBranded));
|
|
569
|
+
|
|
570
|
+
expectType<typeof shapeBranded, MinLengthArray<3, string>>('~=');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('should keep brand, length and positions of a branded tuple', () => {
|
|
574
|
+
const tuple: readonly [string, string | number] = [
|
|
575
|
+
'Ada',
|
|
576
|
+
'Grace',
|
|
577
|
+
] as const;
|
|
578
|
+
|
|
579
|
+
const shapeBrandedTuple = asMinLengthArray(2, tuple);
|
|
580
|
+
|
|
581
|
+
assert.isTrue(allStrings(shapeBrandedTuple));
|
|
582
|
+
|
|
583
|
+
expectType<(typeof shapeBrandedTuple)[0], string>('=');
|
|
584
|
+
|
|
585
|
+
expectType<(typeof shapeBrandedTuple)['length'], 2>('=');
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
439
589
|
});
|
|
440
590
|
|
|
441
591
|
describe(some, () => {
|
|
@@ -1157,7 +1157,7 @@ class IMapMappedClass<K, V, KM extends MapSetKeyType>
|
|
|
1157
1157
|
case 'update': {
|
|
1158
1158
|
const curr = mut_result.get(key);
|
|
1159
1159
|
|
|
1160
|
-
if (!mut_result.has(key)
|
|
1160
|
+
if (curr === undefined || !mut_result.has(key)) {
|
|
1161
1161
|
if (this.#showNotFoundMessage) {
|
|
1162
1162
|
console.warn(
|
|
1163
1163
|
`IMapMapped.withMutations::update: key not found: ${String(key)}`,
|
|
@@ -13,7 +13,7 @@ const testKeyToString = (key: Readonly<TestKey>): string =>
|
|
|
13
13
|
`${key.type}_${key.id}` as const;
|
|
14
14
|
|
|
15
15
|
const stringToTestKey = (str: string): TestKey => {
|
|
16
|
-
const [type, idStr] = str.split('_');
|
|
16
|
+
const [type, idStr] = str.split('_', 2);
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
type: type ?? '',
|
|
@@ -113,7 +113,7 @@ describe('IMapMapped.create', () => {
|
|
|
113
113
|
): string => `${key.nested.id}_${key.arr.join(',')}` as const;
|
|
114
114
|
|
|
115
115
|
const stringToComplexKey = (str: string): ComplexKey => {
|
|
116
|
-
const [idStr, arrStr] = str.split('_');
|
|
116
|
+
const [idStr, arrStr] = str.split('_', 2);
|
|
117
117
|
|
|
118
118
|
return {
|
|
119
119
|
nested: {
|
package/src/collections/imap.mts
CHANGED
|
@@ -877,7 +877,7 @@ class IMapClass<K extends MapSetKeyType, V>
|
|
|
877
877
|
|
|
878
878
|
const curr = mut_result.get(key);
|
|
879
879
|
|
|
880
|
-
if (!mut_result.has(key)
|
|
880
|
+
if (curr === undefined || !mut_result.has(key)) {
|
|
881
881
|
if (this.#showNotFoundMessage) {
|
|
882
882
|
const keyStr = unknownToString(key);
|
|
883
883
|
|
|
@@ -13,7 +13,7 @@ const testElementToString = (el: Readonly<TestElement>): string =>
|
|
|
13
13
|
`${el.type}_${el.id}` as const;
|
|
14
14
|
|
|
15
15
|
const stringToTestElement = (str: string): TestElement => {
|
|
16
|
-
const [type, idStr] = str.split('_');
|
|
16
|
+
const [type, idStr] = str.split('_', 2);
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
type: type ?? '',
|
package/src/functional/match.mts
CHANGED
|
@@ -121,7 +121,7 @@ expectType<AllCasesCovered<'a' | 'b', Readonly<{ a: 1; b: 2; c: 3 }>>, false>(
|
|
|
121
121
|
'=',
|
|
122
122
|
);
|
|
123
123
|
|
|
124
|
-
expectType<AllCasesCovered<string,
|
|
124
|
+
expectType<AllCasesCovered<string, ReadonlyRecord<string, string>>, true>('=');
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* @template Case A union of string literal types.
|
|
@@ -152,9 +152,10 @@ expectType<
|
|
|
152
152
|
false
|
|
153
153
|
>('=');
|
|
154
154
|
|
|
155
|
-
expectType<
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
expectType<
|
|
156
|
+
IsLiteralUnionFullyCovered<string, ReadonlyRecord<string, string>>,
|
|
157
|
+
false
|
|
158
|
+
>('=');
|
|
158
159
|
|
|
159
160
|
expectType<
|
|
160
161
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
@@ -40,9 +40,24 @@ export function map<O extends UnknownOptional, S2>(
|
|
|
40
40
|
): Optional<S2>;
|
|
41
41
|
|
|
42
42
|
// Curried version
|
|
43
|
+
//
|
|
44
|
+
// The returned function is generic over the *whole* optional, mirroring the
|
|
45
|
+
// direct overload above, rather than fixing its parameter to `Optional<S>`.
|
|
46
|
+
// Fixing it makes the curried form unusable from a caller that is itself
|
|
47
|
+
// generic over the optional: with `S` bound to `Unwrap<O>` at currying time
|
|
48
|
+
// the parameter becomes `Optional<Unwrap<O>>`, and TypeScript cannot show a
|
|
49
|
+
// bare `O extends UnknownOptional` is assignable to that — although every
|
|
50
|
+
// concrete instantiation of `O` is.
|
|
51
|
+
//
|
|
52
|
+
// The conditional is what keeps the check: `O` is inferred from the argument
|
|
53
|
+
// and its value type still has to satisfy `S`, so mapping a `(s: string) =>
|
|
54
|
+
// …` over an `Optional<number>` stays an error. The cost is the message it
|
|
55
|
+
// produces — "not assignable to `never`" rather than naming the two optionals.
|
|
43
56
|
export function map<S, S2>(
|
|
44
57
|
mapFn: (value: S) => S2,
|
|
45
|
-
):
|
|
58
|
+
): <O extends UnknownOptional>(
|
|
59
|
+
optional: Unwrap<O> extends S ? O : never,
|
|
60
|
+
) => Optional<S2>;
|
|
46
61
|
|
|
47
62
|
export function map<O extends UnknownOptional, S2>(
|
|
48
63
|
...args:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expectType } from '../expect-type.mjs';
|
|
2
2
|
import { Num } from '../number/index.mjs';
|
|
3
3
|
import { type None, type Some } from '../types.mjs';
|
|
4
|
-
import { Optional } from './optional/index.mjs';
|
|
4
|
+
import { Optional, type UnknownOptional } from './optional/index.mjs';
|
|
5
5
|
import { pipe } from './pipe.mjs';
|
|
6
6
|
import { Result } from './result/index.mjs';
|
|
7
7
|
|
|
@@ -144,6 +144,53 @@ describe('Optional test', () => {
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test('curried form is usable from a caller generic over the optional', () => {
|
|
148
|
+
// The motivating case: with the curried parameter fixed to
|
|
149
|
+
// `Optional<S>`, `S` binds to `Optional.Unwrap<O>` here and a bare
|
|
150
|
+
// `O extends UnknownOptional` is not assignable to
|
|
151
|
+
// `Optional<Optional.Unwrap<O>>`, so this only compiles because the
|
|
152
|
+
// returned function is generic over the whole optional.
|
|
153
|
+
const liftMap =
|
|
154
|
+
<O extends UnknownOptional, B>(mapFn: (x: Optional.Unwrap<O>) => B) =>
|
|
155
|
+
(optional: O): Optional<B> =>
|
|
156
|
+
Optional.map(mapFn)(optional);
|
|
157
|
+
|
|
158
|
+
const lengthOf = liftMap<Optional<string>, number>((s) => s.length);
|
|
159
|
+
|
|
160
|
+
const mapped = lengthOf(Optional.some('abc'));
|
|
161
|
+
|
|
162
|
+
expectType<typeof mapped, Optional<number>>('=');
|
|
163
|
+
|
|
164
|
+
assert.deepStrictEqual(mapped, Optional.some(3));
|
|
165
|
+
|
|
166
|
+
assert.deepStrictEqual(lengthOf(Optional.none), Optional.none);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('curried form still infers the result precisely', () => {
|
|
170
|
+
const mapToLength = Optional.map((text: string) => text.length);
|
|
171
|
+
|
|
172
|
+
const mapped = mapToLength(Optional.some('abc'));
|
|
173
|
+
|
|
174
|
+
expectType<typeof mapped, Optional<number>>('=');
|
|
175
|
+
|
|
176
|
+
assert.deepStrictEqual(mapped, Optional.some(3));
|
|
177
|
+
|
|
178
|
+
const empty = mapToLength(Optional.none);
|
|
179
|
+
|
|
180
|
+
expectType<typeof empty, Optional<number>>('=');
|
|
181
|
+
|
|
182
|
+
assert.deepStrictEqual(empty, Optional.none);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('curried form still rejects a mismatched value type', () => {
|
|
186
|
+
const mapToLength = Optional.map((text: string) => text.length);
|
|
187
|
+
|
|
188
|
+
// @ts-expect-error an Optional<number> is not an Optional<string>
|
|
189
|
+
const rejected = (): unknown => mapToLength(Optional.some(42));
|
|
190
|
+
|
|
191
|
+
expect(rejected).toBeTypeOf('function');
|
|
192
|
+
});
|
|
193
|
+
|
|
147
194
|
test('should preserve types correctly', () => {
|
|
148
195
|
const some = Optional.some(42);
|
|
149
196
|
|
|
@@ -44,9 +44,24 @@ export function mapErr<R extends UnknownResult, E2>(
|
|
|
44
44
|
): Result<UnwrapOk<R>, E2>;
|
|
45
45
|
|
|
46
46
|
// Curried version
|
|
47
|
+
//
|
|
48
|
+
// The returned function is generic over the *whole* result, mirroring the
|
|
49
|
+
// direct overload above, rather than fixing its parameter to `Result<S, E>`.
|
|
50
|
+
// Fixing it makes the curried form unusable from a caller that is itself
|
|
51
|
+
// generic over the result: with `E` bound to `UnwrapErr<R>` at currying time the
|
|
52
|
+
// parameter becomes `Result<...>` in terms of `R`, and TypeScript cannot show
|
|
53
|
+
// a bare `R extends UnknownResult` is assignable to that — although every
|
|
54
|
+
// concrete instantiation of `R` is.
|
|
55
|
+
//
|
|
56
|
+
// The conditional is what keeps the check: `R` is inferred from the argument
|
|
57
|
+
// and its error type still has to satisfy `E`, so the mismatched case stays an
|
|
58
|
+
// error. The cost is the message it produces — "not assignable to `never`"
|
|
59
|
+
// rather than naming the two results.
|
|
47
60
|
export function mapErr<E, E2>(
|
|
48
61
|
mapFn: (error: E) => E2,
|
|
49
|
-
): <
|
|
62
|
+
): <R extends UnknownResult>(
|
|
63
|
+
result: UnwrapErr<R> extends E ? R : never,
|
|
64
|
+
) => Result<UnwrapOk<R>, E2>;
|
|
50
65
|
|
|
51
66
|
export function mapErr<R extends UnknownResult, E2>(
|
|
52
67
|
...args:
|
|
@@ -44,9 +44,24 @@ export function map<R extends UnknownResult, S2>(
|
|
|
44
44
|
): Result<S2, UnwrapErr<R>>;
|
|
45
45
|
|
|
46
46
|
// Curried version
|
|
47
|
+
//
|
|
48
|
+
// The returned function is generic over the *whole* result, mirroring the
|
|
49
|
+
// direct overload above, rather than fixing its parameter to `Result<S, E>`.
|
|
50
|
+
// Fixing it makes the curried form unusable from a caller that is itself
|
|
51
|
+
// generic over the result: with `S` bound to `UnwrapOk<R>` at currying time the
|
|
52
|
+
// parameter becomes `Result<...>` in terms of `R`, and TypeScript cannot show
|
|
53
|
+
// a bare `R extends UnknownResult` is assignable to that — although every
|
|
54
|
+
// concrete instantiation of `R` is.
|
|
55
|
+
//
|
|
56
|
+
// The conditional is what keeps the check: `R` is inferred from the argument
|
|
57
|
+
// and its success type still has to satisfy `S`, so the mismatched case stays an
|
|
58
|
+
// error. The cost is the message it produces — "not assignable to `never`"
|
|
59
|
+
// rather than naming the two results.
|
|
47
60
|
export function map<S, S2>(
|
|
48
61
|
mapFn: (value: S) => S2,
|
|
49
|
-
): <
|
|
62
|
+
): <R extends UnknownResult>(
|
|
63
|
+
result: UnwrapOk<R> extends S ? R : never,
|
|
64
|
+
) => Result<S2, UnwrapErr<R>>;
|
|
50
65
|
|
|
51
66
|
export function map<R extends UnknownResult, S2>(
|
|
52
67
|
...args:
|
|
@@ -3,7 +3,7 @@ import { Num } from '../number/index.mjs';
|
|
|
3
3
|
import { type Err, type Ok } from '../types.mjs';
|
|
4
4
|
import { Optional } from './optional/index.mjs';
|
|
5
5
|
import { pipe } from './pipe.mjs';
|
|
6
|
-
import { Result } from './result/index.mjs';
|
|
6
|
+
import { Result, type UnknownResult } from './result/index.mjs';
|
|
7
7
|
|
|
8
8
|
describe('Result test', () => {
|
|
9
9
|
describe('ok', () => {
|
|
@@ -174,6 +174,49 @@ describe('Result test', () => {
|
|
|
174
174
|
|
|
175
175
|
expect(result.value).toBe('10');
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
test('curried form is usable from a caller generic over the result', () => {
|
|
179
|
+
// The motivating case: with the curried parameter fixed to
|
|
180
|
+
// `Result<S, E>`, `S` binds to `Result.UnwrapOk<R>` here and a bare
|
|
181
|
+
// `R extends UnknownResult` is not assignable to that, so this only
|
|
182
|
+
// compiles because the returned function is generic over the whole
|
|
183
|
+
// result.
|
|
184
|
+
const liftMap =
|
|
185
|
+
<R extends UnknownResult, S2>(mapFn: (x: Result.UnwrapOk<R>) => S2) =>
|
|
186
|
+
(result: R): Result<S2, Result.UnwrapErr<R>> =>
|
|
187
|
+
Result.map(mapFn)(result);
|
|
188
|
+
|
|
189
|
+
const lengthOf = liftMap<Result<string, 'boom'>, number>((s) => s.length);
|
|
190
|
+
|
|
191
|
+
const mapped = lengthOf(Result.ok('abc'));
|
|
192
|
+
|
|
193
|
+
expectType<typeof mapped, Result<number, 'boom'>>('=');
|
|
194
|
+
|
|
195
|
+
assert.deepStrictEqual(mapped, Result.ok(3));
|
|
196
|
+
|
|
197
|
+
assert.deepStrictEqual(lengthOf(Result.err('boom')), Result.err('boom'));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('curried form keeps the error type and rejects a mismatch', () => {
|
|
201
|
+
const mapToLength = Result.map((text: string) => text.length);
|
|
202
|
+
|
|
203
|
+
const mapped = mapToLength(Result.ok('abc'));
|
|
204
|
+
|
|
205
|
+
expectType<typeof mapped, Result<number, never>>('=');
|
|
206
|
+
|
|
207
|
+
assert.deepStrictEqual(mapped, Result.ok(3));
|
|
208
|
+
|
|
209
|
+
const failed = mapToLength(Result.err('boom' as const));
|
|
210
|
+
|
|
211
|
+
expectType<typeof failed, Result<number, 'boom'>>('=');
|
|
212
|
+
|
|
213
|
+
assert.deepStrictEqual(failed, Result.err('boom'));
|
|
214
|
+
|
|
215
|
+
// @ts-expect-error a Result<number, …> is not a Result<string, …>
|
|
216
|
+
const rejected = (): unknown => mapToLength(Result.ok(42));
|
|
217
|
+
|
|
218
|
+
expect(rejected).toBeTypeOf('function');
|
|
219
|
+
});
|
|
177
220
|
});
|
|
178
221
|
|
|
179
222
|
describe('mapErr', () => {
|
|
@@ -234,6 +277,40 @@ describe('Result test', () => {
|
|
|
234
277
|
|
|
235
278
|
expect(result.value).toBe('ERROR: FAILED');
|
|
236
279
|
});
|
|
280
|
+
|
|
281
|
+
test('curried form is usable from a caller generic over the result', () => {
|
|
282
|
+
const liftMapErr =
|
|
283
|
+
<R extends UnknownResult, E2>(mapFn: (x: Result.UnwrapErr<R>) => E2) =>
|
|
284
|
+
(result: R): Result<Result.UnwrapOk<R>, E2> =>
|
|
285
|
+
Result.mapErr(mapFn)(result);
|
|
286
|
+
|
|
287
|
+
const shout = liftMapErr<Result<number, string>, string>((e) =>
|
|
288
|
+
e.toUpperCase(),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const mapped = shout(Result.err('failed'));
|
|
292
|
+
|
|
293
|
+
expectType<typeof mapped, Result<number, string>>('=');
|
|
294
|
+
|
|
295
|
+
assert.deepStrictEqual(mapped, Result.err('FAILED'));
|
|
296
|
+
|
|
297
|
+
assert.deepStrictEqual(shout(Result.ok(1)), Result.ok(1));
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('curried form keeps the ok type and rejects a mismatch', () => {
|
|
301
|
+
const shout = Result.mapErr((e: string) => e.toUpperCase());
|
|
302
|
+
|
|
303
|
+
const mapped = shout(Result.err('failed'));
|
|
304
|
+
|
|
305
|
+
expectType<typeof mapped, Result<never, string>>('=');
|
|
306
|
+
|
|
307
|
+
assert.deepStrictEqual(mapped, Result.err('FAILED'));
|
|
308
|
+
|
|
309
|
+
// @ts-expect-error a Result<…, number> is not a Result<…, string>
|
|
310
|
+
const rejected = (): unknown => shout(Result.err(42));
|
|
311
|
+
|
|
312
|
+
expect(rejected).toBeTypeOf('function');
|
|
313
|
+
});
|
|
237
314
|
});
|
|
238
315
|
|
|
239
316
|
describe('unwrapThrow', () => {
|
|
@@ -407,8 +484,11 @@ describe('Result test', () => {
|
|
|
407
484
|
|
|
408
485
|
describe('fromPromise', () => {
|
|
409
486
|
test('handles async functions that resolve', async () => {
|
|
410
|
-
const asyncFn = async (): Promise<number> =>
|
|
411
|
-
Promise.resolve()
|
|
487
|
+
const asyncFn = async (): Promise<number> => {
|
|
488
|
+
await Promise.resolve();
|
|
489
|
+
|
|
490
|
+
return 42;
|
|
491
|
+
};
|
|
412
492
|
|
|
413
493
|
const result = await Result.fromPromise(asyncFn());
|
|
414
494
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type UnknownRecord } from 'ts-type-forge';
|
|
1
|
+
import { type ReadonlyRecord, type UnknownRecord } from 'ts-type-forge';
|
|
2
2
|
import { expectType } from '../expect-type.mjs';
|
|
3
3
|
import { hasKey } from './has-key.mjs';
|
|
4
4
|
import { isRecord } from './is-record.mjs';
|
|
@@ -31,13 +31,13 @@ describe(isRecord, () => {
|
|
|
31
31
|
describe('arrays are not records', () => {
|
|
32
32
|
test('type-level: arrays are not assignable to UnknownRecord', () => {
|
|
33
33
|
// Arrays have a number index signature but no string index signature,
|
|
34
|
-
// so they are assignable to `
|
|
34
|
+
// so they are assignable to `ReadonlyRecord<number, unknown>` but not to
|
|
35
35
|
// `UnknownRecord` (= `ReadonlyRecord<string, unknown>`).
|
|
36
|
-
expectType<readonly number[],
|
|
36
|
+
expectType<readonly number[], ReadonlyRecord<number, unknown>>('<=');
|
|
37
37
|
|
|
38
38
|
expectType<readonly number[], UnknownRecord>('!<=');
|
|
39
39
|
|
|
40
|
-
expectType<readonly string[],
|
|
40
|
+
expectType<readonly string[], ReadonlyRecord<number, unknown>>('<=');
|
|
41
41
|
|
|
42
42
|
expectType<readonly string[], UnknownRecord>('!<=');
|
|
43
43
|
|
|
@@ -157,7 +157,7 @@ describe(isNumber, () => {
|
|
|
157
157
|
|
|
158
158
|
assert.isTrue(isNumber(42));
|
|
159
159
|
|
|
160
|
-
assert.isTrue(isNumber(-
|
|
160
|
+
assert.isTrue(isNumber(-1.23));
|
|
161
161
|
|
|
162
162
|
assert.isTrue(isNumber(Number.NaN));
|
|
163
163
|
|
|
@@ -322,7 +322,7 @@ describe(isNotNumber, () => {
|
|
|
322
322
|
|
|
323
323
|
assert.isFalse(isNotNumber(42));
|
|
324
324
|
|
|
325
|
-
assert.isFalse(isNotNumber(-
|
|
325
|
+
assert.isFalse(isNotNumber(-1.23));
|
|
326
326
|
|
|
327
327
|
assert.isFalse(isNotNumber(Number.NaN));
|
|
328
328
|
|
|
@@ -12,7 +12,7 @@ describe('FiniteNumber test', () => {
|
|
|
12
12
|
|
|
13
13
|
expect(() => asFiniteNumber(-1)).not.toThrow();
|
|
14
14
|
|
|
15
|
-
expect(() => asFiniteNumber(
|
|
15
|
+
expect(() => asFiniteNumber(1.23)).not.toThrow();
|
|
16
16
|
|
|
17
17
|
expect(() => asFiniteNumber(-2.5)).not.toThrow();
|
|
18
18
|
|
|
@@ -59,7 +59,7 @@ describe('FiniteNumber test', () => {
|
|
|
59
59
|
|
|
60
60
|
assert.isTrue(FiniteNumber.is(-1));
|
|
61
61
|
|
|
62
|
-
assert.isTrue(FiniteNumber.is(
|
|
62
|
+
assert.isTrue(FiniteNumber.is(1.23));
|
|
63
63
|
|
|
64
64
|
assert.isTrue(FiniteNumber.is(-2.5));
|
|
65
65
|
|
|
@@ -13,7 +13,7 @@ describe('NonNegativeFiniteNumber test', () => {
|
|
|
13
13
|
|
|
14
14
|
expect(() => asNonNegativeFiniteNumber(1)).not.toThrow();
|
|
15
15
|
|
|
16
|
-
expect(() => asNonNegativeFiniteNumber(
|
|
16
|
+
expect(() => asNonNegativeFiniteNumber(1.23)).not.toThrow();
|
|
17
17
|
|
|
18
18
|
expect(() => asNonNegativeFiniteNumber(0.5)).not.toThrow();
|
|
19
19
|
|
|
@@ -71,7 +71,7 @@ describe('NonNegativeFiniteNumber test', () => {
|
|
|
71
71
|
|
|
72
72
|
assert.isTrue(isNonNegativeFiniteNumber(1));
|
|
73
73
|
|
|
74
|
-
assert.isTrue(isNonNegativeFiniteNumber(
|
|
74
|
+
assert.isTrue(isNonNegativeFiniteNumber(1.23));
|
|
75
75
|
|
|
76
76
|
assert.isTrue(isNonNegativeFiniteNumber(0.5));
|
|
77
77
|
|
|
@@ -13,17 +13,13 @@ describe('NonPositiveFiniteNumber test', () => {
|
|
|
13
13
|
|
|
14
14
|
expect(() => asNonPositiveFiniteNumber(-1)).not.toThrow();
|
|
15
15
|
|
|
16
|
-
expect(() => asNonPositiveFiniteNumber(-
|
|
16
|
+
expect(() => asNonPositiveFiniteNumber(-1.23)).not.toThrow();
|
|
17
17
|
|
|
18
18
|
expect(() => asNonPositiveFiniteNumber(-0.5)).not.toThrow();
|
|
19
19
|
|
|
20
|
-
expect(() =>
|
|
21
|
-
asNonPositiveFiniteNumber(Number.MIN_VALUE * -1),
|
|
22
|
-
).not.toThrow();
|
|
20
|
+
expect(() => asNonPositiveFiniteNumber(-Number.MIN_VALUE)).not.toThrow();
|
|
23
21
|
|
|
24
|
-
expect(() =>
|
|
25
|
-
asNonPositiveFiniteNumber(Number.MAX_VALUE * -1),
|
|
26
|
-
).not.toThrow();
|
|
22
|
+
expect(() => asNonPositiveFiniteNumber(-Number.MAX_VALUE)).not.toThrow();
|
|
27
23
|
});
|
|
28
24
|
|
|
29
25
|
test('rejects positive numbers', () => {
|
|
@@ -63,11 +59,11 @@ describe('NonPositiveFiniteNumber test', () => {
|
|
|
63
59
|
|
|
64
60
|
assert.isTrue(isNonPositiveFiniteNumber(-1));
|
|
65
61
|
|
|
66
|
-
assert.isTrue(isNonPositiveFiniteNumber(-
|
|
62
|
+
assert.isTrue(isNonPositiveFiniteNumber(-1.23));
|
|
67
63
|
|
|
68
64
|
assert.isTrue(isNonPositiveFiniteNumber(-0.5));
|
|
69
65
|
|
|
70
|
-
assert.isTrue(isNonPositiveFiniteNumber(Number.MIN_VALUE
|
|
66
|
+
assert.isTrue(isNonPositiveFiniteNumber(-Number.MIN_VALUE));
|
|
71
67
|
});
|
|
72
68
|
|
|
73
69
|
test('correctly rejects positive numbers', () => {
|
|
@@ -13,7 +13,7 @@ describe('NonZeroFiniteNumber test', () => {
|
|
|
13
13
|
|
|
14
14
|
expect(() => asNonZeroFiniteNumber(-1)).not.toThrow();
|
|
15
15
|
|
|
16
|
-
expect(() => asNonZeroFiniteNumber(
|
|
16
|
+
expect(() => asNonZeroFiniteNumber(1.23)).not.toThrow();
|
|
17
17
|
|
|
18
18
|
expect(() => asNonZeroFiniteNumber(-2.5)).not.toThrow();
|
|
19
19
|
|
|
@@ -73,7 +73,7 @@ describe('NonZeroFiniteNumber test', () => {
|
|
|
73
73
|
|
|
74
74
|
assert.isTrue(isNonZeroFiniteNumber(-1));
|
|
75
75
|
|
|
76
|
-
assert.isTrue(isNonZeroFiniteNumber(
|
|
76
|
+
assert.isTrue(isNonZeroFiniteNumber(1.23));
|
|
77
77
|
|
|
78
78
|
assert.isTrue(isNonZeroFiniteNumber(-2.5));
|
|
79
79
|
|