coolaj86 / ajquery.js

The fastest, most lightweight, least dependency jQuery alternative.
https://twitter.com/coolaj86/status/1303386788119998464
Mozilla Public License 2.0
80 stars 8 forks source link

types: how to get more specific returns than just "Element"? #10

Open coolaj86 opened 2 months ago

coolaj86 commented 2 months ago

Problem

let input = $("input");
if (!input) {
  throw new Error('no input element selected');
}
input.value = "foo"; // E: Property 'value' does not exist on type 'Element'.

Here's a basic type definition that's unfortunately quite wrong:

/**
 * Select first matching element, just like console $
 * @param {String} cssSelector
 * @param {ParentNode} [$parent=document]
 */
function $(cssSelector, $parent = document) {
  let $child = $parent.querySelector(cssSelector);
  return $child;
}

/**
 * Select all matching child elements as a JS Array, just like console $$
 * @param {String} cssSelector
 * @param {ParentNode} [$parent=document]
 */
function $$(cssSelector, $parent = document) {
  let $children = $parent.querySelectorAll(cssSelector);
  let children = Array.from($children);
  return children;
}
coolaj86 commented 2 months ago

Some thoughts from Grok2, GPT4o, and Claude:

Grok2

// Here we go, creating a declaration file for your selector functions

declare function $<Selector extends string, Selected extends Element = HTMLElement>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected | undefined;

declare function $$<Selector extends string, Selected extends Element = HTMLElement>(
    selectors: Selector | Selector[],
    baseElements?: ParentNode | Iterable<ParentNode>
): Selected[];

// And for those who like to live on the edge with their element selections
declare function expectElement<Selector extends string, Selected extends Element = HTMLElement>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected;

declare function lastElement<Selector extends string, Selected extends Element = HTMLElement>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected | undefined;

declare function elementExists(
    selectors: string | string[],
    baseElement?: ParentNode
): boolean;

// Exporting these functions for the universe to use
export { $, $$, expectElement, lastElement, elementExists };

GPT

// selectors.d.ts

import type { ParseSelector } from 'typed-query-selector/parser.js';

type BaseElements = ParentNode | Iterable<ParentNode>;

export function $<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
  selectors: Selector | Selector[],
  baseElement?: ParentNode
): Selected | undefined;
export function $<Selected extends Element = HTMLElement>(
  selectors: string | string[],
  baseElement?: ParentNode
): Selected | undefined;

export function $$<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
  selectors: Selector | Selector[],
  baseElements?: BaseElements
): Selected[];
export function $$<Selected extends Element = HTMLElement>(
  selectors: string | string[],
  baseElements?: BaseElements
): Selected[];

export function lastElement<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
  selectors: Selector | Selector[],
  baseElement?: ParentNode
): Selected | undefined;
export function lastElement<Selected extends Element = HTMLElement>(
  selectors: string | string[],
  baseElement?: ParentNode
): Selected | undefined;

export function elementExists(
  selectors: string | string[],
  baseElement?: ParentNode,
): boolean;

export function expectElement<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
  selectors: Selector | Selector[],
  baseElement?: ParentNode
): Selected;
export function expectElement<Selected extends Element = HTMLElement>(
  selectors: string | string[],
  baseElement?: ParentNode
): Selected;

export class ElementNotFoundError extends Error {
  name: string;
}
// selectors.js

/**
 * @typedef {import('./selectors').$} $
 * @typedef {import('./selectors').$$} $$
 * @typedef {import('./selectors').lastElement} lastElement
 * @typedef {import('./selectors').elementExists} elementExists
 * @typedef {import('./selectors').expectElement} expectElement
 * @typedef {import('./selectors').ElementNotFoundError} ElementNotFoundError
 */

/**
 * @type {$}
 */
const $ = (selectors, baseElement = document) => baseElement.querySelector(selectors);

/**
 * @type {$$}
 */
const $$ = (selectors, baseElements = document) => Array.from(baseElements.querySelectorAll(selectors));

/**
 * @type {lastElement}
 */
const lastElement = (selectors, baseElement = document) => {
  const elements = baseElement.querySelectorAll(selectors);
  return elements[elements.length - 1] || undefined;
};

/**
 * @type {elementExists}
 */
const elementExists = (selectors, baseElement = document) => !!baseElement.querySelector(selectors);

/**
 * @type {expectElement}
 */
const expectElement = (selectors, baseElement = document) => {
  const element = baseElement.querySelector(selectors);
  if (!element) throw new ElementNotFoundError(`Expected element not found: ${selectors}`);
  return element;
};

/**
 * @constructor
 * @extends {Error}
 */
function ElementNotFoundError(message) {
  this.name = 'ElementNotFoundError';
  this.message = message;
  this.stack = (new Error()).stack;
}
ElementNotFoundError.prototype = Object.create(Error.prototype);
ElementNotFoundError.prototype.constructor = ElementNotFoundError;

// Usage example
try {
  const element = expectElement('button.submit');
  console.log('Found button:', element);
} catch (error) {
  if (error instanceof ElementNotFoundError) {
    console.error('Button not found:', error.message);
  }
}

Claude

import type { ParseSelector } from 'typed-query-selector/parser.js';

type BaseElements = ParentNode | Iterable<ParentNode>;

export function $<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected | undefined;
export function $<Selected extends Element = HTMLElement>(
    selectors: string | string[],
    baseElement?: ParentNode
): Selected | undefined;

export function $$<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
    selectors: Selector | Selector[],
    baseElements?: BaseElements
): Selected[];
export function $$<Selected extends Element = HTMLElement>(
    selectors: string | string[],
    baseElements?: BaseElements
): Selected[];

export class ElementNotFoundError extends Error {
    name: 'ElementNotFoundError';
}

export function expectElement<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected;
export function expectElement<Selected extends Element = HTMLElement>(
    selectors: string | string[],
    baseElement?: ParentNode
): Selected;

export function lastElement<Selector extends string, Selected extends Element = ParseSelector<Selector, HTMLElement>>(
    selectors: Selector | Selector[],
    baseElement?: ParentNode
): Selected | undefined;
export function lastElement<Selected extends Element = HTMLElement>(
    selectors: string | string[],
    baseElement?: ParentNode
): Selected | undefined;

export function elementExists(
    selectors: string | string[],
    baseElement?: ParentNode
): boolean;