# Querying elements in a typesafe manner

If you are like me, you may not like typescript but you do like proper types. With jsdoc, it's easy enough to add proper typing to any bit of vanilla javascript code.

However, things are not so great when you use the `querySelector` and `querySelectorAll` api, because it returns a generic `Element`. How to fix this ?

## The issue

You want typesafe js, so you enable this in your project in the `jsconfig.json`file

```javascript
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["es2017", "dom"],
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": false,
    "noImplicitOverride": false,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true
  },
  "include": ["src/*.js"],
  "exclude": ["node_modules", "./test/"]
}
```

Let's say you have a form with an `input`. Maybe you do something like this:

```javascript
const myInput = document.querySelector('form input');
myInput.checked = true; // you get "Property checked does not exist on Element"
```

The obvious fix: add some inline jsdoc

```javascript
/**
* @type {HTMLInputElement}
*/
const myInput = document.querySelector('form input');
myInput.checked = true; // yay!
```

This works, but it's not so great when you need to deal with collections. Sure, it works, but it's not exactly developer friendly.

```javascript
document.querySelectorAll('form input.checkbox').forEach((input) => {
    input.checked = true; // Huho... again ?
});

document.querySelectorAll('form input.checkbox').forEach(
/**
* @param {HTMLInputElement} input
*/
(input) => {
    input.checked = true; // yay!
});
```

## A better solution?

Introducing `q` the only function you need to query your dom! If you were missing jQuery ease of use, this might just be your new best friend.

```javascript
/**
 * Query elements in a typesafe manner
 *
 * You can declare your owns tags in the HTMLElementTagNameMap namespace
 *
 * ```js
 * // my-tag.d.ts
 * declare global {
 *   interface HTMLElementTagNameMap {
 *      "my-tag": MyTag;
 *   }
 * }
 * ```
 *
 * @template {keyof HTMLElementTagNameMap} K
 * @param {K|String} tagName Name of the element, or global selector string (returns any element).
 * @param {String} selector Selector appended to the type. If it contains a space, type is ignored.
 * @param {Document|HTMLElement} ctx Context (document by default). If a context is specified, :scope is applied
 * @returns {Array<HTMLElementTagNameMap[K]>}
 */
export default function q(tagName, selector = '', ctx = document) {
  // Don't prepend the type of we are asking for children, eg: #my-element type
  selector = selector.includes(' ') ? selector : `${tagName}${selector}`;
  // Needed for direct children queries
  // @link https://developer.mozilla.org/en-US/docs/Web/CSS/:scope#direct_children
  // Needed to avoid inconsistent behaviour
  // @link https://lists.w3.org/Archives/Public/public-webapi/2008Apr/0251.html
  if (!(ctx instanceof Document)) {
    selector = `:scope ${selector}`;
  }
  return Array.from(ctx.querySelectorAll(selector));
}
```

Let's revisit our previous examples

```javascript
const myInput = q('input', 'form input')[0];
myInput.checked = true; // yay!

q('input', 'form input.checkbox').forEach((input) => {
    input.checked = true; // yay!
});
```

Not sure yet if it's worth it to make another function that returns only one element using `querySelector`, but that would be easy enough to create on your own :-)

## Some extra goodies

You might have noticed that the code does a little bit more than that. Let's see the extra goodies included.

You can easily query an element and add more specificity.

```javascript
const inputs = q('input', '.checkbox'); // will query input.checkbox
```

When querying children nodes (as soon as you have a space in the selector), the tag name is ignored.

```javascript
const inputs = q('input', 'form input'); // will query form input
```

Fragments are properly scoped by adding `:scope` automatically. This is almost always the desired behavior because it allows using direct selectors without extra syntax, and it will make sure you don't get elements outside of the context.

This scope issue was so bad in even wondered if it makes sense to allow this third parameter.

```javascript
const form = q('form')[0]; // will query form
const formInputs = q('input', '.container .checkbox', form); // will query :scope .container input.checkbox
const directInputs = q('input', '> .checkbox', form); // will query :scope > input.checkbox
```

You can still query multiple elements if you want.

```javascript
const mixed = q('label,input'); // still working fine since you can pass a regular string, but obviously, you don't get a proper type back
```

You get an array as a result... meaning, you can map, filter... with properly typed element inside your callback function.

```javascript
const allNames = formInputs.map((el) => {
  return el.getAttribute('name');
});
```

You can extend the list of tags if you use custom elements.

```typescript
declare global {
   interface HTMLElementTagNameMap {
      "my-tag": MyTag;
   }
}
```

And let the magic happen.

```javascript
const myTag = q('my-tag'); // who gets a properly typed element ?
```

You can play with it here (check the console).

%[https://codepen.io/lekoalabe/pen/XWxbQJe]
