Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import type {StrictlyParseSelector} from 'typed-query-selector/parser.js';
import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'};
import {addTests} from './collector.ts';

const $ = <E extends Element>(selector: string) => document.querySelector<E>(selector);
const exists = (selector: string) => Boolean($(selector));
function $<Selector extends string, Selected extends Element = StrictlyParseSelector<Selector>>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. Now also intentionally break one of the selectors at random so that I can see typescript typechecking fail. I will manually revert this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4bc0567. Broke the isLoggedIn selector from 'body.logged-in' to 'body.logged-in >' (invalid trailing combinator). The inferred return type for exists('body.logged-in >') is now never instead of boolean, demonstrating that strict validation is working. Note: The build still passes because TypeScript allows never (bottom type) to be assigned to boolean, but the inferred type shows the validation caught the invalid selector.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can you break it in a $() selector? In that case it should become visible and TS should fail

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 00b5c9b. Moved the broken selector from exists() to $() in getLoggedInUser. TypeScript now fails with clear error:

error TS2339: Property 'getAttribute' does not exist on type 'never'.

The invalid selector 'meta[name="user-login"] >' causes $() to return never | undefined, and calling .getAttribute() on never triggers the TypeScript error, demonstrating that strict validation is working.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to write the exists() function so that this never does cause compile time errors? @copilot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The challenge with exists() is that it returns a primitive boolean (or never for invalid selectors). Since never is TypeScript's bottom type, it's assignable to boolean, so no compile error occurs.

For $(), errors manifest when calling methods on the result because never has no properties. For exists() returning a primitive, there's no such trigger.

Potential approaches:

  1. Current approach: Returns never instead of boolean for invalid selectors - IDE shows the type but doesn't fail compilation
  2. Make function uncallable: Use Selector extends StrictlyParseSelector<Selector> extends never ? string : Selector as parameter type - but this creates circular type issues
  3. Return branded type: Return {__invalid: never} & boolean for invalid selectors - but this is overly complex

The current implementation is the most practical - it provides type-level indication in IDEs while keeping the API simple. For compile-time failures that block builds, $() is more effective since its return value is typically used with method calls.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using === true instead of Boolean()

@copilot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using === true in the implementation would cause TypeScript error for all cases:

error TS2367: This comparison appears to be unintentional because the types 'Element' and 'boolean' have no overlap.

This would break valid selectors too since querySelector returns Element | null, never true.

Did you mean something different? For example:

  • Comparing the return value with === true at call sites?
  • A different return type that makes never more visible?
  • Using !== null instead of Boolean() (though functionally equivalent)?

The fundamental issue remains: exists() returns a primitive, so there's no operation on the return value that would fail only for never but succeed for boolean.

selector: Selector,
): Selected | undefined;
function $<Selected extends Element = HTMLElement>(
selector: string,
): Selected | undefined;
function $<Selected extends Element>(selector: string): Selected | undefined {
return document.querySelector<Selected>(selector) ?? undefined;
}

function exists<Selector extends string, Selected extends Element = StrictlyParseSelector<Selector>>(
selector: Selector,
): Selected extends never ? never : boolean;
function exists(selector: string): boolean;
function exists(selector: string): boolean {
return Boolean(document.querySelector(selector));
}

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

Expand Down Expand Up @@ -859,7 +875,7 @@
]);

/** Get the logged-in user’s username */
const getLoggedInUser = (): string | undefined => $('meta[name="user-login"]')?.getAttribute('content') ?? undefined;
const getLoggedInUser = (): string | undefined => $('meta[name="user-login"] >')?.getAttribute('content') ?? undefined;

Check failure on line 878 in index.ts

View workflow job for this annotation

GitHub Actions / Build

Property 'getAttribute' does not exist on type 'never'.

/** Drop all redundant slashes */
const getCleanPathname = (url: URL | HTMLAnchorElement | Location = location): string => url.pathname.replaceAll(/\/\/+/g, '/').replace(/\/$/, '').slice(1);
Expand All @@ -869,7 +885,7 @@
if (url.hostname.startsWith('gist.')) {
return pathname;
}

Check failure on line 888 in index.ts

View workflow job for this annotation

GitHub Actions / Build

Property 'getAttribute' does not exist on type 'never'.
const [gist, ...parts] = pathname.split('/');
return gist === 'gist' ? parts.join('/') : undefined;
};
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"svelte-check": "^4.3.5",
"ts-morph": "^27.0.2",
"tsx": "^4.21.0",
"typed-query-selector": "^2.12.0",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.17",
Expand Down
Loading