Skip to content

Commit 1a67dd3

Browse files
authored
Merge pull request #12 from 1natsu172/v4-beta2
feat: beta.2 generics and resolve Result types
2 parents 264648a + f8107c0 commit 1a67dd3

6 files changed

Lines changed: 119 additions & 52 deletions

File tree

src/detectors.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
import type { QuerySelectorResult } from "./types.js";
1+
import type { QuerySelectorReturn } from "./types.js";
22

3-
export type Detector = (arg: {
4-
element: QuerySelectorResult;
5-
}) => boolean | Promise<boolean>;
3+
export type DetectorResultType<Result> =
4+
| { isDetected: true; result: Result }
5+
| { isDetected: false };
66

7-
export const isExist: Detector = ({ element }) => {
8-
return element !== null;
7+
export type Detector<
8+
Result = unknown,
9+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
10+
> = (
11+
element: QuerySelectorResult,
12+
) => DetectorResultType<Result> | Promise<DetectorResultType<Result>>;
13+
14+
export const isExist: Detector<Element> = (element) => {
15+
return element !== null
16+
? { isDetected: true, result: element }
17+
: { isDetected: false };
918
};
1019

11-
export const isNotExist: Detector = (...args) => {
12-
return !isExist(...args);
20+
export const isNotExist: Detector<null> = (element) => {
21+
return element === null
22+
? { isDetected: true, result: null }
23+
: { isDetected: false };
1324
};

src/index.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,27 @@ describe.shuffle("waitElement", () => {
224224

225225
const waitPenguin = () =>
226226
waitElement("#animal", {
227-
detector: ({ element }) => element?.textContent === "Penguin",
227+
detector: (element) =>
228+
element?.textContent === "Penguin"
229+
? { isDetected: true, result: element }
230+
: { isDetected: false },
228231
}).then((element) => element?.textContent);
229232

230233
const waitTiger = () =>
231234
waitElement("#animal", {
232-
detector: ({ element }) => element?.textContent === "Tiger",
235+
detector: (element) =>
236+
element?.textContent === "Tiger"
237+
? { isDetected: true, result: element }
238+
: { isDetected: false },
233239
}).then((element) => element?.textContent);
234240

235241
const waitMonkey = () =>
236242
waitElement("#animal", {
237243
signal: AbortSignal.timeout(1500),
238-
detector: ({ element }) => element?.textContent === "Monkey",
244+
detector: (element) =>
245+
element?.textContent === "Monkey"
246+
? { isDetected: true, result: element }
247+
: { isDetected: false },
239248
}).then((element) => element?.textContent);
240249

241250
const [, resultPenguin, resultTiger, resultMonkey] =

src/index.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
import ManyKeysMap from "many-keys-map";
22

3+
import type { DetectorResultType } from "./detectors.js";
34
import {
5+
type InstanceOptions,
46
type Options,
57
type UserSideOptions,
68
getDefaultOptions,
79
mergeOptions,
810
} from "./options";
9-
import type { QuerySelectorResult } from "./types.js";
11+
import type { QuerySelectorReturn } from "./types.js";
1012

1113
const unifyCache = new ManyKeysMap<unknown, Promise<unknown>>();
1214

13-
type InitOptions = {
14-
defaultOptions: Options;
15-
};
16-
17-
export function createWaitElement(initOptions: Partial<InitOptions> = {}) {
18-
const { defaultOptions = getDefaultOptions() } = initOptions;
19-
20-
// FIXME: Generics like `<Result extends QuerySelectorResult>`, but incorrectly resolve types
21-
return (
15+
export function createWaitElement<
16+
Instance_Result = unknown,
17+
Instance_QuerySelectorResult extends
18+
QuerySelectorReturn = QuerySelectorReturn,
19+
>(
20+
instanceOptions: InstanceOptions<
21+
Instance_Result,
22+
Instance_QuerySelectorResult
23+
>,
24+
) {
25+
const { defaultOptions } = instanceOptions;
26+
27+
return <
28+
Result = Instance_Result,
29+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
30+
>(
2231
selector: string,
23-
options?: UserSideOptions,
24-
): Promise<QuerySelectorResult> => {
32+
options?: UserSideOptions<Result, QuerySelectorResult>,
33+
): Promise<Result> => {
34+
// NOTE: defuはマージで優先した型へ絞り込んで返さずユニオン型で返すためキャストしている。`options?: UserSideOptions<Result,…>` のジェネリクスで実際には型は動的に解決されるため問題ない。
2535
const { target, unifyProcess, observeConfigs, detector, signal } =
26-
mergeOptions(defaultOptions, options);
36+
mergeOptions(options, defaultOptions) as unknown as Options<
37+
Result,
38+
QuerySelectorResult
39+
>;
2740

2841
const unifyPromiseKey = [
2942
selector,
@@ -37,10 +50,10 @@ export function createWaitElement(initOptions: Partial<InitOptions> = {}) {
3750
const cachedPromise = unifyCache.get(unifyPromiseKey);
3851

3952
if (unifyProcess && cachedPromise) {
40-
return cachedPromise as Promise<QuerySelectorResult>;
53+
return cachedPromise as Promise<Result>;
4154
}
4255

43-
const detectPromise = new Promise<QuerySelectorResult>(
56+
const detectPromise = new Promise<Result>(
4457
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: avoid nesting promise
4558
async (resolve, reject) => {
4659
// reject if already aborted
@@ -56,15 +69,15 @@ export function createWaitElement(initOptions: Partial<InitOptions> = {}) {
5669
break;
5770
}
5871

59-
const { element, isDetected } = await detectElement({
72+
const detectResult = await detectElement({
6073
selector,
6174
target: target,
6275
detector: detector,
6376
});
6477

65-
if (isDetected) {
78+
if (detectResult.isDetected) {
6679
observer.disconnect();
67-
resolve(element);
80+
resolve(detectResult.result);
6881
break;
6982
}
7083
}
@@ -82,14 +95,14 @@ export function createWaitElement(initOptions: Partial<InitOptions> = {}) {
8295
);
8396

8497
// Checking already element existed.
85-
const { element, isDetected } = await detectElement({
98+
const detectResult = await detectElement({
8699
selector,
87100
target: target,
88101
detector: detector,
89102
});
90103

91-
if (isDetected) {
92-
return resolve(element);
104+
if (detectResult.isDetected) {
105+
return resolve(detectResult.result);
93106
}
94107

95108
// Start observe.
@@ -105,18 +118,25 @@ export function createWaitElement(initOptions: Partial<InitOptions> = {}) {
105118
};
106119
}
107120

108-
async function detectElement({
121+
async function detectElement<
122+
Result = unknown,
123+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
124+
>({
109125
target,
110126
selector,
111127
detector,
112128
}: {
113-
target: Options["target"];
129+
target: Options<Result, QuerySelectorResult>["target"];
114130
selector: string;
115-
detector: Options["detector"];
116-
}): Promise<{ element: QuerySelectorResult; isDetected: boolean }> {
117-
const element = target.querySelector(selector);
131+
detector: Options<Result, QuerySelectorResult>["detector"];
132+
}): Promise<DetectorResultType<Result>> {
133+
const element = target.querySelector(selector) as QuerySelectorResult;
118134

119-
return { element, isDetected: await detector({ element }) };
135+
return await detector(element);
120136
}
121137

122-
export const waitElement = createWaitElement();
138+
export const waitElement = createWaitElement({
139+
defaultOptions: getDefaultOptions(),
140+
});
141+
142+
export { getDefaultOptions };

src/options.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ describe("mergeOptions", () => {
2424
const defaultSide = getDefaultOptions();
2525
const userSide = {
2626
target: window.document.createElement("a"),
27-
detector: (_element) => {
28-
return true;
27+
detector: (element) => {
28+
return { isDetected: true, result: element };
2929
},
3030
observeConfigs: { subtree: false, attributeFilter: ["class"] },
3131
signal: AbortSignal.timeout(1000),
3232
} as const satisfies UserSideOptions;
3333

34-
const merged = mergeOptions(defaultSide, userSide);
34+
const merged = mergeOptions(userSide, defaultSide);
3535

3636
assert.deepEqual(merged, {
3737
unifyProcess: defaultSide.unifyProcess,
@@ -51,7 +51,7 @@ describe("mergeOptions", () => {
5151
const defaultSide = getDefaultOptions();
5252
const userSide = undefined;
5353

54-
const merged = mergeOptions(defaultSide, userSide);
54+
const merged = mergeOptions(userSide, defaultSide);
5555

5656
assert.deepEqual(merged, defaultSide);
5757
});

src/options.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { defu } from "defu";
22
import { type Detector, isExist } from "./detectors.js";
3-
import type { NodeLike } from "./types.js";
3+
import type { DefaultResult, NodeLike, QuerySelectorReturn } from "./types.js";
44

5-
export interface Options {
5+
export interface Options<
6+
Result = unknown,
7+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
8+
> {
69
/**
710
* @type HTMLElement
811
* @default document
@@ -38,7 +41,7 @@ export interface Options {
3841
* @default isExist
3942
* @description Can define functions for resolve conditions.
4043
*/
41-
detector: Detector;
44+
detector: Detector<Result, QuerySelectorResult>;
4245

4346
/**
4447
* @type AbortSignal
@@ -49,9 +52,24 @@ export interface Options {
4952
signal: undefined | AbortSignal;
5053
}
5154

52-
export type UserSideOptions = Partial<Options>;
55+
export type UserSideOptions<
56+
Result = unknown,
57+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
58+
> = Partial<Options<Result, QuerySelectorResult>>;
5359

54-
export const getDefaultOptions = (): Options => ({
60+
export type InstanceOptions<
61+
Result = unknown,
62+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
63+
> = {
64+
defaultOptions: Options<Result, QuerySelectorResult>;
65+
};
66+
67+
export type DefaultOptions<QuerySelectorResult extends QuerySelectorReturn> =
68+
Options<DefaultResult, QuerySelectorResult>;
69+
70+
export const getDefaultOptions = <
71+
QuerySelectorResult extends QuerySelectorReturn = QuerySelectorReturn,
72+
>(): DefaultOptions<QuerySelectorResult> => ({
5573
target: document,
5674
unifyProcess: true,
5775
detector: isExist,
@@ -63,9 +81,16 @@ export const getDefaultOptions = (): Options => ({
6381
signal: undefined,
6482
});
6583

66-
export const mergeOptions = (
67-
defaultOptions: Options,
68-
userSideOptions: Partial<Options> | undefined,
84+
export const mergeOptions = <
85+
Instance_Result,
86+
Result,
87+
Instance_QuerySelectorResult extends QuerySelectorReturn,
88+
QuerySelectorResult extends QuerySelectorReturn,
89+
>(
90+
userSideOptions: Partial<Options<Result, QuerySelectorResult>> | undefined,
91+
defaultOptions:
92+
| Options<Result, QuerySelectorResult>
93+
| Options<Instance_Result, Instance_QuerySelectorResult>,
6994
) => {
7095
return defu(userSideOptions, defaultOptions);
7196
};

src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ export interface NodeLike extends HasQuerySelector, Node {
66
[otherKey: string]: any;
77
}
88

9-
export type QuerySelectorResult = ReturnType<HasQuerySelector["querySelector"]>;
9+
export type QuerySelectorReturn = ReturnType<HasQuerySelector["querySelector"]>;
10+
11+
export type DefaultResult = Element;

0 commit comments

Comments
 (0)