Skip to content

Commit edae8bd

Browse files
Copilotfregante
andcommitted
Use strict selector types for $ and exists helpers
Updated $ and exists helpers to use StrictlyParseSelector type from typed-query-selector, enabling compile-time validation of CSS selectors. Both helpers now accept template literal selector strings and infer the correct element types, while $ maintains an overload for explicit type specification when needed. Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
1 parent 72790dc commit edae8bd

File tree

4 files changed

+53
-6
lines changed

4 files changed

+53
-6
lines changed

global.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable @typescript-eslint/consistent-type-definitions -- Module augmentation */
2-
/// <reference path="./node_modules/typed-query-selector/strict.d.ts" />
32

43
// Broaden types because testing against `"undefined"` is fine for our regexes
54
interface RegExp {

index.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'};
22
import {addTests} from './collector.ts';
3-
4-
// Selector helpers with typed-query-selector validation.
5-
// The generic parameter allows type override when selector inference isn't specific enough (e.g., attribute-only selectors)
6-
const $ = <E extends Element = Element>(selector: string) => document.querySelector<E>(selector);
7-
const exists = (selector: string): boolean => Boolean(document.querySelector(selector));
3+
import type {StrictlyParseSelector} from './strict-types.ts';
4+
5+
// Selector helpers with typed-query-selector strict validation
6+
// Overload 1: Strict validation with type inference from selector
7+
function $<S extends string, E extends StrictlyParseSelector<S>>(
8+
selector: S,
9+
): [E] extends [never] ? never : E | undefined;
10+
// Overload 2: Allow explicit type override when inference isn't specific enough
11+
function $<E extends Element>(selector: string): E | undefined;
12+
// Implementation
13+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type is inferred from overloads
14+
function $(selector: string) {
15+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Return type validated by strict typing
16+
return document.querySelector(selector);
17+
}
18+
19+
// @ts-expect-error -- E is inferred by TypeScript automatically, not used explicitly
20+
function exists<S extends string, E extends StrictlyParseSelector<S>>(selector: S): boolean {
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- querySelector return type validated by strict typing
22+
const element = document.querySelector(selector);
23+
return Boolean(element);
24+
}
825

926
const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB()
1027

@@ -297,6 +314,7 @@ TEST: addTests('isQuickPR', [
297314
'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1',
298315
]);
299316

317+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- Strict validation may mark complex selectors as potentially invalid
300318
const getStateLabel = (): string | undefined => $([
301319
'.State', // Old view
302320
// React versions
@@ -402,6 +420,7 @@ export const isEmptyRepo = (): boolean => exists('[aria-label="Cannot fork becau
402420

403421
export const isPublicRepo = (): boolean => exists('meta[name="octolytics-dimension-repository_public"][content="true"]');
404422

423+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Strict validation may mark selectors with child combinators as potentially invalid
405424
export const isArchivedRepo = (): boolean => Boolean(isRepo() && $('main > .flash-warn')?.textContent!.includes('archived'));
406425

407426
export const isBlank = (): boolean => exists('main .blankslate:not([hidden] .blankslate)');
@@ -861,6 +880,7 @@ TEST: addTests('isNewRepoTemplate', [
861880
]);
862881

863882
/** Get the logged-in user’s username */
883+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- getAttribute is safe
864884
const getLoggedInUser = (): string | undefined => $('meta[name="user-login"]')?.getAttribute('content') ?? undefined;
865885

866886
/** Drop all redundant slashes */

strict-types.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Import the StrictlyParseSelector type for strict validation
2+
import type {StrictlyParseSelector} from 'typed-query-selector/parser';
3+
4+
// Import strict types from typed-query-selector
5+
/* eslint-disable @typescript-eslint/consistent-type-definitions -- Global augmentation requires interface */
6+
declare global {
7+
interface ParentNode {
8+
querySelector<S extends string, E extends StrictlyParseSelector<S>>(
9+
selector: S,
10+
): [E] extends [never] ? never : E | undefined;
11+
12+
querySelectorAll<S extends string, E extends StrictlyParseSelector<S>>(
13+
selector: S,
14+
): [E] extends [never] ? never : NodeListOf<E>;
15+
}
16+
17+
interface Element {
18+
closest<S extends string, E extends StrictlyParseSelector<S>>(
19+
selector: S,
20+
): [E] extends [never] ? never : E | undefined;
21+
}
22+
}
23+
/* eslint-enable @typescript-eslint/consistent-type-definitions */
24+
25+
// Export the type for use in index.ts
26+
27+
export {type StrictlyParseSelector} from 'typed-query-selector/parser';

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"index.ts",
99
"index.test.ts",
1010
"global.d.ts",
11+
"strict-types.d.ts",
1112
"collector.ts"
1213
]
1314
}

0 commit comments

Comments
 (0)