Skip to content

Commit

Permalink
allow null; update tests and README; bump to v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Warner authored and Michael Warner committed Sep 14, 2023
1 parent e61c14d commit 1901069
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 383 deletions.
62 changes: 20 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,43 @@

- [el-is](#el-is)
- [Install](#install)
- [Requirements](#requirements)
- [Usage](#usage)
- [Usage Notes](#usage-notes)
- [API](#api)
- [elIs](#elis)
- [Type parameters](#type-parameters)
- [Parameters](#parameters)
- [Returns](#returns)

## Install

```bash
npm install @michaelallenwarner/el-is
```

## Requirements

- TypeScript >= 5.0 (assuming you're using TypeScript)

## Usage

```ts
import { elIs } from '@michaelallenwarner/el-is';

const htmlElement = document.querySelector('section'); // `HTMLElement | null`
if (htmlElement && elIs(htmlElement, 'SECTION')) {
if (elIs(htmlElement, 'SECTION')) {
// `htmlElement` now has type `HTMLElement & { tagName: 'SECTION' }`
}

const mathMlElement = document.querySelector('mrow'); // `MathMLElement | null`
if (mathMlElement && elIs(mathMlElement, 'mrow')) {
if (elIs(mathMlElement, 'mrow')) {
// `mathMlElement` now has type `MathMLElement & { tagName: 'mrow' }`
}

const element = document.querySelector('.svg-path-element'); // `Element | null`
if (element && elIs(element, 'path')) {
if (elIs(element, 'path')) {
// `element` now has type `Element & { tagName: 'path' }`
/*
But probably just do `if (element instanceof SVGPathElement)` instead!
Also, although `Element` will work as a type for the first parameter,
it's better to narrow it first to `HTMLElement`, `MathMLElement`, or `SVGElement`.
it's advisable to narrow it first to `HTMLElement`, `MathMLElement`, or `SVGElement`.
See Usage Notes below.
*/
Expand All @@ -47,43 +47,21 @@ if (element && elIs(element, 'path')) {

## Usage Notes

The `elIs()` function takes a DOM element (`el`) and a string-literal (`tagName`) and returns the `boolean` result of a simple `el.tagName === tagName` check, but with some TypeScript niceties described below.
The `elIs()` function takes two arguments—a maybe-`null` DOM element (`el`) and a string literal (`tagName`)and returns the `boolean` result of a simple `el?.tagName === tagName` check, but with some TypeScript niceties described below.

But first, a quick warning: _**you probably should only use this function if the element-type you're checking against doesn't have its own dedicated interface!**_ For example, if you want to check whether `el` is a link, I can't think of any reason not to just do `if (el instanceof HTMLAnchorElement)`, so that the type-narrowing gives you all the properties and methods that come with the more specific interface (like `HTMLAnchorElement.href`). The `elIs()` function is really for checking against element-types that _don't_ have their own interfaces, like `<section>`.
But first, a quick warning: _**you probably should only use this function if the kind of element you're checking against doesn't have its own dedicated interface!**_ For example, if you want to check whether `el` is a link, I can't think of any reason not to just do `if (el instanceof HTMLAnchorElement)`, so that the type-narrowing gives you all the properties and methods that come with the more specific interface (like `HTMLAnchorElement.href`). The `elIs()` function is really for testing whether an element is of a kind that _doesn't_ have its own interface, like `<section>`.

With that out of the way, here are the TypeScript niceties you get with the `elIs()` function:
With that out of the way, here are the TypeScript niceties you get with `elIs()`:

- In the `true` case, the function's type-predicate narrows the type of `el` by intersecting its input-type with `{ tagName: T }`, where `T` is the string-literal type provided as the `tagName` argument.
- To prevent you from making typos (and to enable autocomplete), the `tagName` parameter is restricted to valid tag-name values for built-in elements of the `Element`-subtype that `el` is an instance of (if applicable). The supported `Element`-subtypes are `HTMLElement`, `SVGElement`, and `MathMLElement`. For example, if `el` is an instance of `HTMLElement`, then `tagName` must be a string literal that's the value of the `tagName` property of a built-in HTML element. If `el` is an `Element` but not an instance of one of those subtypes, then `tagName` is still restricted, but less so: it must be a string literal that's the value of the `tagName` property of a built-in HTML element, SVG element, or MathML element. (It's best if `el` is more specific than `Element`, though, so that you don't have to worry about confusing uppercase HTML tag-names like `'A'` with lowercase SVG or MathML tag-names like `'a'`.)
- When it returns `true`, the function narrows `el`'s type by ruling out `null` (if applicable) and intersecting with `{ tagName: T }`, where `T` is the string-literal type provided as the `tagName` argument. So if the `el` you supply has type `HTMLElement` (or `HTMLElement | null`), then if `elIs(el, 'DIV')` returns `true`, the type of `el` will be narrowed to `HTMLElement & { tagName: 'DIV' }`.
- You get autocomplete functionality and typo-prevention with the `tagName` parameter.

Specifically, the `tagName` parameter must be the value of a standard DOM element's `tagName` property, and this restriction further depends on the type of the `el` argument you supply. For example, if `el` is an instance of `HTMLElement`, then the supplied `tagName` argument must be the value of a standard HTML element's `tagName` property, like `'DIV'`. Supported `el` types are `HTMLElement`, `SVGElement`, `MathMLElement`, the more general `Element`, and unions of any of these; the `tagName` argument will always be appropriately restricted. (It's best, though, if `el` is just `HTMLElement`, `SVGElement`, or `MathMLElement`, so that you don't have to worry about confusing uppercase HTML tag-names like `'A'` with lowercase SVG or MathML tag-names like `'a'`.)

If you don't use `elIs()`, you can just do something like `if (el.matches('section'))` or `if (el.tagName === 'section')` instead. But then you don't get the type-narrowing, the autocomplete, or the typo-prevention, and you'll probably make the mistake I just made in the previous sentence (should be `if (el.tagName === 'SECTION')`).
If you don't use `elIs()`, you can just do something like `if (el.matches('section'))` or `if (el.tagName === 'section')` instead. But then you don't get the type-narrowing, the autocomplete, or the typo-prevention, and you'll probably make the mistake I made in the previous sentence (should be `if (el.tagName === 'SECTION')`).

A few more notes:

- Because of the aforementioned `tagName` restrictions, this function won't work with custom elements.
- Although `SVGElement` is a supported `Element`-subtype here, it's worth noting that, at the time of writing, every kind of SVG element has its own dedicated interface (like `SVGPathElement`), so you probably want to reach for `instanceof` when checking against it.
- Deprecated HTML elements are supported (e.g., if `el` is an `HTMLElement` or `Element`, then `tagName` can be `'MARQUEE'`).

## API

### elIs

**elIs**<`E`, `T`\>(`el`, `tagName`): `el` is `E` & { `tagName`: `T` }

#### Type parameters

| Name | Type |
| :------ | :------ |
| `E` | extends `HTMLElement` \| `Element` \| `SVGElement` \| `MathMLElement` |
| `T` | extends `Uppercase<keyof HTMLElementTagNameMap \| keyof HTMLElementDeprecatedTagNameMap>` \| keyof `SVGElementTagNameMap` \| keyof `MathMLElementTagNameMap` |

#### Parameters

| Name | Type |
| :------ | :------ |
| `el` | `E` |
| `tagName` | `T` |

#### Returns

`el` is `E` & { `tagName`: `T` }
- Because of the `tagName` restrictions, this function won't work with custom elements in TypeScript.
- Although `SVGElement` is a supported type for the `el` parameter, it's worth noting that, at the time of writing, every kind of SVG element has its own dedicated interface (like `SVGPathElement`), so you probably want to reach for `instanceof` with SVG tags.
- Deprecated HTML elements are supported (e.g., if `el` is an `HTMLElement`, then `tagName` can be `'MARQUEE'`).
Loading

0 comments on commit 1901069

Please sign in to comment.