ts-data-forge 14.4.0 → 14.5.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 +6 -0
- package/dist/array/impl/array-utils-search.d.mts.map +1 -1
- package/dist/array/impl/array-utils-search.mjs.map +1 -1
- package/dist/functional/optional/impl/index.d.mts +1 -0
- package/dist/functional/optional/impl/index.d.mts.map +1 -1
- package/dist/functional/optional/impl/index.mjs +1 -0
- package/dist/functional/optional/impl/index.mjs.map +1 -1
- package/dist/functional/optional/impl/optional-match.d.mts +48 -0
- package/dist/functional/optional/impl/optional-match.d.mts.map +1 -0
- package/dist/functional/optional/impl/optional-match.mjs +22 -0
- package/dist/functional/optional/impl/optional-match.mjs.map +1 -0
- package/dist/functional/result/impl/index.d.mts +4 -0
- package/dist/functional/result/impl/index.d.mts.map +1 -1
- package/dist/functional/result/impl/index.mjs +4 -0
- package/dist/functional/result/impl/index.mjs.map +1 -1
- package/dist/functional/result/impl/result-from-optional.d.mts +41 -0
- package/dist/functional/result/impl/result-from-optional.d.mts.map +1 -0
- package/dist/functional/result/impl/result-from-optional.mjs +24 -0
- package/dist/functional/result/impl/result-from-optional.mjs.map +1 -0
- package/dist/functional/result/impl/result-match.d.mts +47 -0
- package/dist/functional/result/impl/result-match.d.mts.map +1 -0
- package/dist/functional/result/impl/result-match.mjs +24 -0
- package/dist/functional/result/impl/result-match.mjs.map +1 -0
- package/dist/functional/result/impl/result-safe-try.d.mts +51 -0
- package/dist/functional/result/impl/result-safe-try.d.mts.map +1 -0
- package/dist/functional/result/impl/result-safe-try.mjs +24 -0
- package/dist/functional/result/impl/result-safe-try.mjs.map +1 -0
- package/dist/functional/result/impl/result-safe-unwrap.d.mts +36 -0
- package/dist/functional/result/impl/result-safe-unwrap.d.mts.map +1 -0
- package/dist/functional/result/impl/result-safe-unwrap.mjs +14 -0
- package/dist/functional/result/impl/result-safe-unwrap.mjs.map +1 -0
- package/dist/others/debounce-function.d.mts.map +1 -1
- package/dist/others/debounce-function.mjs.map +1 -1
- package/dist/others/fast-deep-equal.d.mts.map +1 -1
- package/dist/others/fast-deep-equal.mjs +1 -1
- package/dist/others/fast-deep-equal.mjs.map +1 -1
- package/package.json +9 -9
- package/src/array/impl/array-utils-modification.test.mts +3 -3
- package/src/array/impl/array-utils-search.mts +22 -8
- package/src/array/impl/array-utils-shape-invariants.test.mts +3 -3
- package/src/array/impl/array-utils-transformation.test.mts +1 -1
- package/src/functional/optional/impl/index.mts +1 -0
- package/src/functional/optional/impl/optional-match.mts +108 -0
- package/src/functional/optional.test.mts +79 -0
- package/src/functional/result/impl/index.mts +4 -0
- package/src/functional/result/impl/result-from-optional.mts +80 -0
- package/src/functional/result/impl/result-match.mts +108 -0
- package/src/functional/result/impl/result-safe-try.mts +110 -0
- package/src/functional/result/impl/result-safe-unwrap.mts +50 -0
- package/src/functional/result.test.mts +331 -0
- package/src/guard/is-error.test.mts +7 -7
- package/src/others/debounce-function.mts +5 -1
- package/src/others/fast-deep-equal.mts +9 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { hasKey, isRecord } from '../../../guard/index.mjs';
|
|
2
|
+
import { type Err } from '../../../types.mjs';
|
|
3
|
+
import { type Result, type UnknownResult } from '../result.mjs';
|
|
4
|
+
import { type UnwrapErr, type UnwrapOk } from './types.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs a generator body in which `yield* Result.safeUnwrap(...)` unwraps `Ok`
|
|
8
|
+
* values and short-circuits on the first `Err`, emulating Rust's `?` operator
|
|
9
|
+
* (early-return error propagation).
|
|
10
|
+
*
|
|
11
|
+
* The body must return a `Result`. If every unwrapped `Result` is `Ok`, the
|
|
12
|
+
* returned value is the body's `Result`; otherwise it is the first `Err`
|
|
13
|
+
* encountered, and the rest of the body never runs — though the generator is
|
|
14
|
+
* closed rather than dropped, so `finally` blocks and `using` disposals
|
|
15
|
+
* pending in the body still run. An exception thrown by the body is not
|
|
16
|
+
* converted to an `Err`: it propagates out of `safeTry` (as a rejected
|
|
17
|
+
* `Promise` for an async body). Use {@link Result.fromThrowable} for that.
|
|
18
|
+
*
|
|
19
|
+
* Passing an async generator (`async function*`) returns a
|
|
20
|
+
* `Promise<Result<...>>` instead, so `await` can be used inside the body.
|
|
21
|
+
* Note that `yield*` inside an `async function*` awaits what it receives, so
|
|
22
|
+
* unwrapping an `Ok` whose value is itself a `Promise` yields the awaited
|
|
23
|
+
* value there while the static type still says `Promise<...>`; keep promises
|
|
24
|
+
* out of the `Ok` side of a `Result` unwrapped in an async body.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const parseTwo = (a: string, b: string): Result<number, Error> =>
|
|
30
|
+
* Result.safeTry(function* () {
|
|
31
|
+
* const x = yield* Result.safeUnwrap(Num.safeParseInt(a));
|
|
32
|
+
*
|
|
33
|
+
* const y = yield* Result.safeUnwrap(Num.safeParseInt(b));
|
|
34
|
+
*
|
|
35
|
+
* return Result.ok(x + y);
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* assert.deepStrictEqual(parseTwo('1', '2'), Result.ok(3));
|
|
39
|
+
*
|
|
40
|
+
* assert.isTrue(Result.isErr(parseTwo('1', 'not a number')));
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @template Y The union of `Err` types unwrapped via
|
|
44
|
+
* {@link Result.safeUnwrap} (`never` if nothing is unwrapped).
|
|
45
|
+
* @template R2 The `Result` type returned by the body.
|
|
46
|
+
* @param body A generator function whose `yield*` expressions delegate to
|
|
47
|
+
* {@link Result.safeUnwrap} and whose return value is a `Result`.
|
|
48
|
+
* @returns The body's `Result`, or the first `Err` short-circuited out of it
|
|
49
|
+
* (wrapped in a `Promise` for an async generator body).
|
|
50
|
+
*/
|
|
51
|
+
export function safeTry<Y extends Err<unknown>, R2 extends UnknownResult>(
|
|
52
|
+
body: () => Generator<Y, R2>,
|
|
53
|
+
): Result<UnwrapOk<R2>, UnwrapErr<R2> | UnwrapErr<Y>>;
|
|
54
|
+
|
|
55
|
+
export function safeTry<Y extends Err<unknown>, R2 extends UnknownResult>(
|
|
56
|
+
body: () => AsyncGenerator<Y, R2>,
|
|
57
|
+
): Promise<Result<UnwrapOk<R2>, UnwrapErr<R2> | UnwrapErr<Y>>>;
|
|
58
|
+
|
|
59
|
+
export function safeTry(
|
|
60
|
+
body:
|
|
61
|
+
| (() => AsyncGenerator<UnknownResult, UnknownResult>)
|
|
62
|
+
| (() => Generator<UnknownResult, UnknownResult>),
|
|
63
|
+
): Promise<UnknownResult> | UnknownResult {
|
|
64
|
+
const generator = body();
|
|
65
|
+
|
|
66
|
+
// A yielded `Err` and a returned `Result` are both already the final
|
|
67
|
+
// answer, so the first step's `value` is the result in either case: the
|
|
68
|
+
// generator is closed rather than resumed after yielding.
|
|
69
|
+
const step = generator.next();
|
|
70
|
+
|
|
71
|
+
if (isSyncStep(step)) {
|
|
72
|
+
// Dropping a suspended generator would skip the `finally` blocks and
|
|
73
|
+
// `using` disposals still pending in the body; closing it runs them
|
|
74
|
+
// before the short-circuited `Err` is handed back. `isSyncGenerator` is
|
|
75
|
+
// what makes `return()` here synchronous rather than a floating promise.
|
|
76
|
+
if (step.done !== true && isSyncGenerator(generator, step)) {
|
|
77
|
+
generator.return(step.value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return step.value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return step.then(async (asyncStep) => {
|
|
84
|
+
if (asyncStep.done !== true) {
|
|
85
|
+
await generator.return(asyncStep.value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return asyncStep.value;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A sync generator's `next()` returns a plain `{ value, done }` object with
|
|
93
|
+
// own properties, while an async generator's returns a `Promise`, whose
|
|
94
|
+
// `then` lives on the prototype — so an own-key check on `done` separates the
|
|
95
|
+
// two.
|
|
96
|
+
const isSyncStep = (
|
|
97
|
+
value: unknown,
|
|
98
|
+
): value is IteratorResult<UnknownResult, UnknownResult> =>
|
|
99
|
+
isRecord(value) && hasKey(value, 'done');
|
|
100
|
+
|
|
101
|
+
// The generator-side counterpart of `isSyncStep`: a step in the sync form can
|
|
102
|
+
// only have come from a sync generator, so the step is what narrows the
|
|
103
|
+
// generator that produced it — hence the predicate reads its second argument
|
|
104
|
+
// and not its first.
|
|
105
|
+
const isSyncGenerator = (
|
|
106
|
+
_generator:
|
|
107
|
+
| AsyncGenerator<UnknownResult, UnknownResult>
|
|
108
|
+
| Generator<UnknownResult, UnknownResult>,
|
|
109
|
+
step: unknown,
|
|
110
|
+
): _generator is Generator<UnknownResult, UnknownResult> => isSyncStep(step);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type UnknownResult } from '../result.mjs';
|
|
2
|
+
import { isErr } from './result-is-err.mjs';
|
|
3
|
+
import { type NarrowToErr, type UnwrapOk } from './types.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unwraps a `Result` inside a {@link Result.safeTry} generator body via
|
|
7
|
+
* `yield*`, emulating Rust's `?` operator.
|
|
8
|
+
*
|
|
9
|
+
* If the `Result` is `Ok`, the `yield*` expression evaluates to the contained
|
|
10
|
+
* value and execution continues. If it is `Err`, the error is yielded to
|
|
11
|
+
* {@link Result.safeTry}, which returns it immediately — the rest of the
|
|
12
|
+
* generator body never runs.
|
|
13
|
+
*
|
|
14
|
+
* This generator must only be delegated to (`yield*`) inside a
|
|
15
|
+
* {@link Result.safeTry} body; resuming it after it has yielded an `Err`
|
|
16
|
+
* throws.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const result = Result.safeTry(function* () {
|
|
22
|
+
* const x = yield* Result.safeUnwrap(Result.ok(2));
|
|
23
|
+
*
|
|
24
|
+
* const y = yield* Result.safeUnwrap(Result.ok(3));
|
|
25
|
+
*
|
|
26
|
+
* return Result.ok(x + y);
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* assert.deepStrictEqual(result, Result.ok(5));
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @template R The input `UnknownResult` type.
|
|
33
|
+
* @param result The `Result` to unwrap.
|
|
34
|
+
* @returns A generator that yields the `Err` (if any) and returns the `Ok`
|
|
35
|
+
* value.
|
|
36
|
+
*/
|
|
37
|
+
export function* safeUnwrap<R extends UnknownResult>(
|
|
38
|
+
result: R,
|
|
39
|
+
): Generator<NarrowToErr<R>, UnwrapOk<R>, unknown> {
|
|
40
|
+
if (isErr(result)) {
|
|
41
|
+
yield result;
|
|
42
|
+
|
|
43
|
+
throw new Error(
|
|
44
|
+
'`safeUnwrap()` generator was resumed after yielding an `Err`; it must only be used via `yield*` inside `safeTry()`',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
49
|
+
return result.value as UnwrapOk<R>;
|
|
50
|
+
}
|
|
@@ -1017,4 +1017,335 @@ describe('Result test', () => {
|
|
|
1017
1017
|
>();
|
|
1018
1018
|
});
|
|
1019
1019
|
});
|
|
1020
|
+
|
|
1021
|
+
describe('match', () => {
|
|
1022
|
+
test('should apply the ok case for Ok', () => {
|
|
1023
|
+
const result = Result.match(Result.ok(21), {
|
|
1024
|
+
ok: (value) => value * 2,
|
|
1025
|
+
err: () => 0,
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
expectType<typeof result, number>('=');
|
|
1029
|
+
|
|
1030
|
+
assert.deepStrictEqual(result, 42);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test('should apply the err case for Err', () => {
|
|
1034
|
+
const result = Result.match(Result.err('failure'), {
|
|
1035
|
+
ok: () => 'ok',
|
|
1036
|
+
err: (error) => `error: ${error}`,
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
assert.deepStrictEqual(result, 'error: failure');
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test('should support Result union inputs', () => {
|
|
1043
|
+
// Created through a function so that the const is not flow-narrowed to
|
|
1044
|
+
// the Ok side.
|
|
1045
|
+
const makeResult = (): Result<number, string> => Result.ok(2);
|
|
1046
|
+
|
|
1047
|
+
const result = makeResult();
|
|
1048
|
+
|
|
1049
|
+
const folded = Result.match(result, {
|
|
1050
|
+
ok: (value) => value + 1,
|
|
1051
|
+
err: (error) => error.length,
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
expectType<typeof folded, number>('=');
|
|
1055
|
+
|
|
1056
|
+
assert.deepStrictEqual(folded, 3);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test('curried version should work', () => {
|
|
1060
|
+
const matcher = Result.match({
|
|
1061
|
+
ok: (value: number) => value + 1,
|
|
1062
|
+
err: (error: string) => error.length,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
assert.deepStrictEqual(matcher(Result.ok(1)), 2);
|
|
1066
|
+
|
|
1067
|
+
assert.deepStrictEqual(matcher(Result.err('oops')), 4);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test('curried version should compose with Array.map and pipe', () => {
|
|
1071
|
+
const matcher = Result.match({
|
|
1072
|
+
ok: (value: number) => `ok:${value}`,
|
|
1073
|
+
err: (error: string) => `err:${error}`,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const results: readonly Result<number, string>[] = [
|
|
1077
|
+
Result.ok(1),
|
|
1078
|
+
Result.err('bad'),
|
|
1079
|
+
] as const;
|
|
1080
|
+
|
|
1081
|
+
assert.deepStrictEqual(results.map(matcher), ['ok:1', 'err:bad']);
|
|
1082
|
+
|
|
1083
|
+
const single: Result<number, string> = Result.ok(3);
|
|
1084
|
+
|
|
1085
|
+
assert.deepStrictEqual(pipe(single).map(matcher).value, 'ok:3');
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
describe('fromOptional', () => {
|
|
1090
|
+
test('should convert Some to Ok', () => {
|
|
1091
|
+
const result = Result.fromOptional(Optional.some(42), 'missing');
|
|
1092
|
+
|
|
1093
|
+
expectType<typeof result, Result<42, 'missing'>>('=');
|
|
1094
|
+
|
|
1095
|
+
assert.deepStrictEqual(result, Result.ok(42));
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test('should convert None to Err with the provided error', () => {
|
|
1099
|
+
const result = Result.fromOptional(Optional.none, 'missing');
|
|
1100
|
+
|
|
1101
|
+
assert.isTrue(Result.isErr(result));
|
|
1102
|
+
|
|
1103
|
+
assert.deepStrictEqual(result.value, 'missing');
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test('should convert Some(undefined) to Ok(undefined), not Err', () => {
|
|
1107
|
+
const makeOptional = (): Optional<undefined> => Optional.some(undefined);
|
|
1108
|
+
|
|
1109
|
+
const result = Result.fromOptional(makeOptional(), 'missing');
|
|
1110
|
+
|
|
1111
|
+
assert.isTrue(Result.isOk(result));
|
|
1112
|
+
|
|
1113
|
+
assert.deepStrictEqual(result.value, undefined);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test('should support Optional union inputs', () => {
|
|
1117
|
+
// Created through a function so that the const is not flow-narrowed to
|
|
1118
|
+
// the Some side.
|
|
1119
|
+
const makeOptional = (): Optional<number> => Optional.some(2);
|
|
1120
|
+
|
|
1121
|
+
const result = Result.fromOptional(makeOptional(), 'missing');
|
|
1122
|
+
|
|
1123
|
+
expectType<typeof result, Result<number, 'missing'>>('=');
|
|
1124
|
+
|
|
1125
|
+
assert.deepStrictEqual(result, Result.ok(2));
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test('curried version should work', () => {
|
|
1129
|
+
const withMissingError = Result.fromOptional('missing');
|
|
1130
|
+
|
|
1131
|
+
const result = withMissingError(Optional.some(1));
|
|
1132
|
+
|
|
1133
|
+
expectType<typeof result, Result<1, 'missing'>>('=');
|
|
1134
|
+
|
|
1135
|
+
assert.deepStrictEqual(result, Result.ok(1));
|
|
1136
|
+
|
|
1137
|
+
assert.deepStrictEqual(
|
|
1138
|
+
withMissingError(Optional.none),
|
|
1139
|
+
Result.err('missing'),
|
|
1140
|
+
);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
describe('safeTry', () => {
|
|
1145
|
+
test('should return the body result when every yielded Result is Ok', () => {
|
|
1146
|
+
const result = Result.safeTry(function* () {
|
|
1147
|
+
const x = yield* Result.safeUnwrap(Result.ok(2));
|
|
1148
|
+
|
|
1149
|
+
const y = yield* Result.safeUnwrap(Result.ok(3));
|
|
1150
|
+
|
|
1151
|
+
return Result.ok(x + y);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
expectType<typeof result, Result<number, never>>('=');
|
|
1155
|
+
|
|
1156
|
+
assert.deepStrictEqual(result, Result.ok(5));
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
test('should short-circuit on the first Err', () => {
|
|
1160
|
+
const makeFailing = (): Result<number, string> =>
|
|
1161
|
+
Result.err('first error');
|
|
1162
|
+
|
|
1163
|
+
const failing = makeFailing();
|
|
1164
|
+
|
|
1165
|
+
let mut_reached = false;
|
|
1166
|
+
|
|
1167
|
+
const result = Result.safeTry(function* () {
|
|
1168
|
+
const x = yield* Result.safeUnwrap(failing);
|
|
1169
|
+
|
|
1170
|
+
mut_reached = true;
|
|
1171
|
+
|
|
1172
|
+
return Result.ok(x);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
assert.isFalse(mut_reached);
|
|
1176
|
+
|
|
1177
|
+
assert.deepStrictEqual(result, Result.err('first error'));
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test('should collect error types from yields and the return', () => {
|
|
1181
|
+
const makeSource = (): Result<number, 'e1'> => Result.ok(1);
|
|
1182
|
+
|
|
1183
|
+
const source = makeSource();
|
|
1184
|
+
|
|
1185
|
+
const result = Result.safeTry(function* () {
|
|
1186
|
+
const x = yield* Result.safeUnwrap(source);
|
|
1187
|
+
|
|
1188
|
+
if (x > 0) {
|
|
1189
|
+
return Result.err('e2');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return Result.ok(x);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
expectType<typeof result, Result<number, 'e1' | 'e2'>>('=');
|
|
1196
|
+
|
|
1197
|
+
assert.deepStrictEqual(result, Result.err('e2'));
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
test('async generator should return a Promise of the body result', async () => {
|
|
1201
|
+
const result = await Result.safeTry(async function* () {
|
|
1202
|
+
const x = yield* Result.safeUnwrap(
|
|
1203
|
+
await Result.fromPromise(Promise.resolve(10)),
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
return Result.ok(x + 1);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
assert.deepStrictEqual(result, Result.ok(11));
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
test('async generator should short-circuit on Err', async () => {
|
|
1213
|
+
const makeFailing = (): Promise<Result<number, string>> =>
|
|
1214
|
+
Promise.resolve(Result.err('async error'));
|
|
1215
|
+
|
|
1216
|
+
let mut_reached = false;
|
|
1217
|
+
|
|
1218
|
+
const promise = Result.safeTry(async function* () {
|
|
1219
|
+
const x = yield* Result.safeUnwrap(await makeFailing());
|
|
1220
|
+
|
|
1221
|
+
mut_reached = true;
|
|
1222
|
+
|
|
1223
|
+
return Result.ok(x);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
expectType<typeof promise, Promise<Result<number, string>>>('=');
|
|
1227
|
+
|
|
1228
|
+
const result = await promise;
|
|
1229
|
+
|
|
1230
|
+
assert.isFalse(mut_reached);
|
|
1231
|
+
|
|
1232
|
+
assert.deepStrictEqual(result, Result.err('async error'));
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
test('should run pending `finally` blocks when short-circuiting', () => {
|
|
1236
|
+
// Created through a function so that the const is not flow-narrowed to
|
|
1237
|
+
// the Err side.
|
|
1238
|
+
const makeFailing = (): Result<number, string> => Result.err('boom');
|
|
1239
|
+
|
|
1240
|
+
let mut_cleanedUp = false;
|
|
1241
|
+
|
|
1242
|
+
const result = Result.safeTry(function* () {
|
|
1243
|
+
try {
|
|
1244
|
+
const x = yield* Result.safeUnwrap(makeFailing());
|
|
1245
|
+
|
|
1246
|
+
return Result.ok(x);
|
|
1247
|
+
} finally {
|
|
1248
|
+
mut_cleanedUp = true;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
assert.isTrue(mut_cleanedUp);
|
|
1253
|
+
|
|
1254
|
+
assert.deepStrictEqual(result, Result.err('boom'));
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
test('should propagate an exception thrown by the body', () => {
|
|
1258
|
+
expect(() =>
|
|
1259
|
+
Result.safeTry(function* () {
|
|
1260
|
+
const x = yield* Result.safeUnwrap(Result.ok(1));
|
|
1261
|
+
|
|
1262
|
+
throw new Error(`boom: ${x}`);
|
|
1263
|
+
}),
|
|
1264
|
+
).toThrow('boom: 1');
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test('async generator should reject on an exception thrown by the body', async () => {
|
|
1268
|
+
await expect(
|
|
1269
|
+
Result.safeTry(async function* () {
|
|
1270
|
+
const x = yield* Result.safeUnwrap(
|
|
1271
|
+
await Promise.resolve(Result.ok(1)),
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
throw new Error(`boom: ${x}`);
|
|
1275
|
+
}),
|
|
1276
|
+
).rejects.toThrow('boom: 1');
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
test('should propagate through a delegating helper generator', () => {
|
|
1280
|
+
const makeSource = (): Result<number, 'e1'> => Result.err('e1');
|
|
1281
|
+
|
|
1282
|
+
const doubled = function* (): Generator<Err<'e1'>, number, unknown> {
|
|
1283
|
+
const x = yield* Result.safeUnwrap(makeSource());
|
|
1284
|
+
|
|
1285
|
+
return x * 2;
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const result = Result.safeTry(function* () {
|
|
1289
|
+
const y = yield* doubled();
|
|
1290
|
+
|
|
1291
|
+
return Result.ok(y);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
expectType<typeof result, Result<number, 'e1'>>('=');
|
|
1295
|
+
|
|
1296
|
+
assert.deepStrictEqual(result, Result.err('e1'));
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test('async generator should run pending `finally` blocks too', async () => {
|
|
1300
|
+
const makeFailing = (): Promise<Result<number, string>> =>
|
|
1301
|
+
Promise.resolve(Result.err('boom'));
|
|
1302
|
+
|
|
1303
|
+
let mut_cleanedUp = false;
|
|
1304
|
+
|
|
1305
|
+
const result = await Result.safeTry(async function* () {
|
|
1306
|
+
try {
|
|
1307
|
+
const x = yield* Result.safeUnwrap(await makeFailing());
|
|
1308
|
+
|
|
1309
|
+
return Result.ok(x);
|
|
1310
|
+
} finally {
|
|
1311
|
+
mut_cleanedUp = true;
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
assert.isTrue(mut_cleanedUp);
|
|
1316
|
+
|
|
1317
|
+
assert.deepStrictEqual(result, Result.err('boom'));
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
describe('safeUnwrap', () => {
|
|
1322
|
+
test('should return the value without yielding for an Ok', () => {
|
|
1323
|
+
const generator = Result.safeUnwrap(Result.ok(7));
|
|
1324
|
+
|
|
1325
|
+
assert.deepStrictEqual(generator.next(), { done: true, value: 7 });
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
test('should yield the Err itself for an Err', () => {
|
|
1329
|
+
const makeFailing = (): Result<number, string> => Result.err('boom');
|
|
1330
|
+
|
|
1331
|
+
const generator = Result.safeUnwrap(makeFailing());
|
|
1332
|
+
|
|
1333
|
+
assert.deepStrictEqual(generator.next(), {
|
|
1334
|
+
done: false,
|
|
1335
|
+
value: Result.err('boom'),
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test('should throw if resumed after yielding an Err', () => {
|
|
1340
|
+
const makeFailing = (): Result<number, string> => Result.err('boom');
|
|
1341
|
+
|
|
1342
|
+
const generator = Result.safeUnwrap(makeFailing());
|
|
1343
|
+
|
|
1344
|
+
generator.next();
|
|
1345
|
+
|
|
1346
|
+
expect(() => generator.next()).toThrow(
|
|
1347
|
+
'it must only be used via `yield*` inside `safeTry()`',
|
|
1348
|
+
);
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1020
1351
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable functional/immutable-data */
|
|
2
1
|
/* eslint-disable functional/no-class-inheritance */
|
|
3
2
|
/* eslint-disable unicorn/error-message */
|
|
4
3
|
import { isError as isErrorBySindreSorhus } from '@sindresorhus/is';
|
|
@@ -129,14 +128,15 @@ describe('isError from @sindresorhus/is', () => {
|
|
|
129
128
|
});
|
|
130
129
|
|
|
131
130
|
test('should return false for errors created with Object.create', () => {
|
|
132
|
-
//
|
|
133
|
-
|
|
131
|
+
// Built and then re-pointed rather than `Object.create(Error.prototype)`,
|
|
132
|
+
// which the strict standard library types as `unknown` — correctly, since
|
|
133
|
+
// it knows nothing about the resulting shape. The object under test is the
|
|
134
|
+
// same: Error.prototype in its chain, never constructed as an Error.
|
|
135
|
+
const mut_protoError = { message: 'proto error', name: 'ProtoError' };
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
protoError.message = 'proto error';
|
|
137
|
+
Object.setPrototypeOf(mut_protoError, Error.prototype);
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
protoError.name = 'ProtoError';
|
|
139
|
+
const protoError = mut_protoError;
|
|
140
140
|
|
|
141
141
|
assert.isFalse(isErrorBySindreSorhus(protoError));
|
|
142
142
|
});
|
|
@@ -11,7 +11,11 @@ export const debounce = <Args extends readonly unknown[]>(
|
|
|
11
11
|
func: (...args: Args) => void,
|
|
12
12
|
waitMilliseconds: number,
|
|
13
13
|
): ((...args: Args) => void) => {
|
|
14
|
-
|
|
14
|
+
// Not `ReturnType<typeof setTimeout>`: with both the DOM and Node
|
|
15
|
+
// declarations in scope that resolves to `unknown`, which `clearTimeout`
|
|
16
|
+
// will not take. Naming what `clearTimeout` accepts says the same thing and
|
|
17
|
+
// stays true in either environment.
|
|
18
|
+
let mut_timeoutId: Parameters<typeof clearTimeout>[0];
|
|
15
19
|
|
|
16
20
|
return (...args: Args): void => {
|
|
17
21
|
clearTimeout(mut_timeoutId);
|
|
@@ -94,6 +94,13 @@ export const fastDeepEqual = <T,>(a: T, b: T): boolean => {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// true if both NaN, false otherwise
|
|
98
|
-
|
|
97
|
+
// true if both NaN, false otherwise. `Number.isNaN` takes a number: called
|
|
98
|
+
// with anything else it is always false, so the check belongs behind a
|
|
99
|
+
// `typeof` guard rather than relying on that.
|
|
100
|
+
return (
|
|
101
|
+
typeof a === 'number' &&
|
|
102
|
+
typeof b === 'number' &&
|
|
103
|
+
Number.isNaN(a) &&
|
|
104
|
+
Number.isNaN(b)
|
|
105
|
+
);
|
|
99
106
|
};
|