Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 24, 2026

Adds typed-query-selector in strict mode to catch invalid CSS selectors at compile time, preventing issues like those in PR #120 and commit 1a04193.

Changes

  • Added typed-query-selector@2.12.0 as dev dependency
  • Updated $ helper with function overloads for strict selector validation:
    • Overload 1: Strict validation with automatic type inference from selector string using StrictlyParseSelector<Selector>
    • Overload 2: Fallback overload for cases where type can't be inferred
    • Implementation: Uses querySelector directly, returning undefined for null values
  • Updated exists helper with function overloads for strict selector validation:
    • Overload 1: Strict validation using StrictlyParseSelector<Selector>, returns never for invalid selectors or boolean for valid ones
    • Overload 2: Fallback overload for cases where type can't be inferred
    • Implementation: Uses querySelector directly for boolean checks
  • Demonstration: Intentionally broke getLoggedInUser selector (changed from 'meta[name="user-login"]' to 'meta[name="user-login"] >') to demonstrate validation causing TypeScript to fail - will be manually reverted

TypeScript Error Demonstrates Validation

The broken selector causes TypeScript to fail with:

index.ts:878:83 - error TS2339: Property 'getAttribute' does not exist on type 'never'.

878 const getLoggedInUser = (): string | undefined => $('meta[name="user-login"] >')?.getAttribute('content') ?? undefined;
                                                                                      ~~~~~~~~~~~~

This shows that:

  1. Invalid selector 'meta[name="user-login"] >' is caught by StrictlyParseSelector
  2. Return type becomes never | undefined instead of expected element type
  3. Calling .getAttribute() on never causes clear TypeScript error
  4. Prevents invalid selectors from being merged

How it works

The implementation follows the select-dom pattern with function overloads and template literal types:

function $<Selector extends string, Selected extends Element = StrictlyParseSelector<Selector>>(
    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));
}

TypeScript captures literal selector strings and validates them at compile time using StrictlyParseSelector:

// ✅ Valid selector - compiles and infers HTMLDivElement | undefined
const valid = $('div.test');

// ❌ Invalid selector - returns never | undefined, preventing use
const invalid = $('div >');  // Trailing combinator
invalid.getAttribute('id'); // Error: Property 'getAttribute' does not exist on type 'never'

// ✅ Valid selector - returns boolean
const validExists = exists('body.logged-in');

// ❌ Invalid selector - returns never instead of boolean
const invalidExists = exists('body.logged-in >');  // Trailing combinator

Key design decisions:

  • No global type modifications - validation happens through function overloads only
  • Template literal types (Selector extends string) capture the exact selector for validation
  • StrictlyParseSelector<Selector> parses and validates selectors at compile-time
  • Invalid selectors return never, preventing their use
  • Valid selectors return the correctly inferred element type
  • For exists(), invalid selectors return never instead of boolean, providing type-level indication of validation failure

Note: The strict parser catches some but not all invalid selectors (e.g., trailing combinators like 'div >' are caught, but unclosed brackets like 'div[test' may not be). This is a known limitation of compile-time CSS parsing for performance reasons.

Original prompt

This section details on the original issue you should resolve

<issue_title>Add typed-query-selector/strict locally to ensure selectors are valid</issue_title>
<issue_description>https://github.com/g-plane/typed-query-selector#strict-mode

This type should make TS fail if a selector is invalid. This would avoid:

cc @yakov116 </issue_description>

<agent_instructions> the $ and exists helpers also need to be updated, most likely using typeof document.querySelector and/or document.querySelector.bind(document) instead of the arrow function</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits January 24, 2026 09:46
Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
Copilot AI changed the title [WIP] Add typed-query-selector/strict locally for valid selectors Add typed-query-selector/strict for compile-time selector validation Jan 24, 2026
Copilot AI requested a review from fregante January 24, 2026 09:52
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>
Copilot AI requested a review from fregante January 24, 2026 10:06
@fregante
Copy link
Member

Do not alter the global types of querySelector
Do not ignore typescript and "unsafe ESLint" errors
Do not use string as a type for the parameters of $() and exists().

if you fail at any of these, you failed the task.

Refer to https://github.com/fregante/select-dom/blob/eae58c91f961e4dffd6f92f52735c8007b452c6d/index.ts#L26 for the implementation example (except it should use the strict parser rather than the regular parser)

@copilot

- Use StrictlyParseSelector for compile-time selector validation
- Follow select-dom pattern with function overloads
- No global type modifications
- No eslint-disable comments
- Template literal types for selector parameters

Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 25, 2026

Do not alter the global types of querySelector
Do not ignore typescript and "unsafe ESLint" errors
Do not use string as a type for the parameters of $() and exists().
...

Fixed in 5412d1b. Implemented following the select-dom pattern:

  • Removed strict-types.d.ts (no global type modifications)
  • Removed all eslint-disable comments
  • Used template literal types (Selector extends string) with StrictlyParseSelector<Selector> for strict validation
  • Function overloads match the reference implementation pattern

@fregante fregante marked this pull request as ready for review January 25, 2026 07:12

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.

- Added function overloads to exists() with StrictlyParseSelector
- Returns never when selector is invalid, boolean when valid
- Intentionally broke isLoggedIn selector to demonstrate validation
- Selector 'body.logged-in >' returns never (invalid trailing combinator)

Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
- Reverted isLoggedIn selector back to valid: 'body.logged-in'
- Broke getLoggedInUser selector: 'meta[name="user-login"] >'
- TypeScript now fails with: "Property 'getAttribute' does not exist on type 'never'"
- This clearly demonstrates strict validation is working

Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add typed-query-selector/strict locally to ensure selectors are valid

2 participants