Skip to content

Commit 05e7e93

Browse files
authored
fix: improve methods chaining compatibility (#2)
1 parent 1bf7eed commit 05e7e93

File tree

2 files changed

+82
-31
lines changed

2 files changed

+82
-31
lines changed

src/DeferredPromise.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export type DeferredPromiseState = "pending" | "resolved" | "rejected";
22
export type ResolveFunction<Data extends any, Result = void> = (
33
data: Data
4-
) => Result;
5-
export type RejectFunction<Result = void> = (reason?: unknown) => Result;
4+
) => Result | PromiseLike<Result>;
5+
export type RejectFunction<Result = void> = (
6+
reason?: unknown
7+
) => Result | PromiseLike<Result>;
68

79
/**
810
* Represents the completion of an asynchronous operation.
@@ -16,7 +18,7 @@ export type RejectFunction<Result = void> = (reason?: unknown) => Result;
1618
* const portReady = new DeferredPromise()
1719
* portReady.reject(new Error('Port is already in use'))
1820
*/
19-
export class DeferredPromise<Data extends unknown = void> {
21+
export class DeferredPromise<Data extends any = void> {
2022
public resolve: ResolveFunction<Data>;
2123
public reject: RejectFunction;
2224
public state: DeferredPromiseState;
@@ -60,20 +62,23 @@ export class DeferredPromise<Data extends unknown = void> {
6062
/**
6163
* Attaches callbacks for the resolution and/or rejection of the Promise.
6264
*/
63-
public then(
64-
onresolved?: ResolveFunction<Data, any>,
65-
onrejected?: RejectFunction
66-
) {
67-
this.promise = this.promise.then(onresolved, onrejected);
68-
return this;
65+
public then<ResolveData = Data, RejectionReason = never>(
66+
onresolved?: ResolveFunction<Data, ResolveData>,
67+
onrejected?: RejectFunction<RejectionReason>
68+
): DeferredPromise<ResolveData | RejectionReason> {
69+
this.promise = this.promise.then<ResolveData, RejectionReason>(
70+
onresolved,
71+
onrejected
72+
);
73+
return this as DeferredPromise<ResolveData | RejectionReason>;
6974
}
7075

7176
/**
7277
* Attaches a callback for only the rejection of the Promise.
7378
*/
7479
public catch<RejectReason = never>(
7580
onrejected?: RejectFunction<RejectReason>
76-
): this {
81+
): DeferredPromise<Data | RejectReason> {
7782
this.promise = this.promise.catch<RejectReason>(onrejected);
7883
return this;
7984
}
@@ -83,7 +88,7 @@ export class DeferredPromise<Data extends unknown = void> {
8388
* the Promise is settled (fulfilled or rejected). The resolved
8489
* value cannot be modified from the callback.
8590
*/
86-
public finally(onfinally?: () => void): this {
91+
public finally(onfinally?: () => void): DeferredPromise<Data> {
8792
this.promise = this.promise.finally(onfinally);
8893
return this;
8994
}

test/DeferredPromise.test.ts

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,82 @@
11
import { DeferredPromise } from "../src";
22

3-
it('can be listened to with ".then()"', (done) => {
4-
expect.assertions(1);
3+
describe("Promise-compliance", () => {
4+
it('can be listened to with ".then()"', (done) => {
5+
expect.assertions(1);
56

6-
const promise = new DeferredPromise<number>();
7+
const promise = new DeferredPromise<number>();
8+
9+
promise.then((data) => {
10+
expect(data).toBe(123);
11+
done();
12+
});
13+
14+
promise.resolve(123);
15+
});
16+
17+
it('can be listened to with ".catch()"', (done) => {
18+
expect.assertions(1);
19+
20+
const promise = new DeferredPromise<number>();
21+
promise.catch((reason) => {
22+
expect(reason).toBe("error");
23+
done();
24+
});
725

8-
promise.then((data) => {
26+
promise.reject("error");
27+
});
28+
29+
it("can be awaited with async/await", async () => {
30+
const promise = new DeferredPromise<number>();
31+
promise.resolve(123);
32+
33+
const data = await promise;
934
expect(data).toBe(123);
10-
done();
1135
});
1236

13-
promise.resolve(123);
14-
});
37+
it('allows data transformation in the ".then()" chain', async () => {
38+
const promise = new DeferredPromise<number>();
1539

16-
it('can be listened to with ".catch()"', (done) => {
17-
expect.assertions(1);
40+
promise.then((value) => value * 2).then((value) => value + 10);
41+
promise.resolve(5);
1842

19-
const promise = new DeferredPromise<number>();
20-
promise.catch((reason) => {
21-
expect(reason).toBe("error");
22-
done();
43+
const number = await promise;
44+
45+
expect(number).toBe(20);
2346
});
2447

25-
promise.reject("error");
26-
});
48+
it('allows ".catch().then()" chaining', async () => {
49+
const promise = new DeferredPromise<number>();
50+
51+
promise
52+
.catch<number>((value) => {
53+
if (typeof value === "number") {
54+
return value;
55+
}
56+
})
57+
.then((value) => value + 10);
58+
59+
promise.reject(5);
60+
const number = await promise;
61+
62+
expect(number).toBe(15);
63+
});
2764

28-
it("can be awaited", async () => {
29-
const promise = new DeferredPromise<number>();
30-
promise.resolve(123);
65+
it('does not alter resolved data with ".finally()"', async () => {
66+
const promise = new DeferredPromise<number>();
67+
68+
const finallyCallback = jest.fn(() => "unexpected");
69+
const wrapper = (): Promise<number> => {
70+
return promise.finally(finallyCallback);
71+
};
72+
73+
promise.resolve(123);
74+
const result = await wrapper();
3175

32-
const data = await promise;
33-
expect(data).toBe(123);
76+
expect(result).toBe(123);
77+
expect(finallyCallback).toHaveBeenCalledTimes(1);
78+
expect(finallyCallback).toHaveBeenCalledWith();
79+
});
3480
});
3581

3682
describe("resolve()", () => {

0 commit comments

Comments
 (0)