Skip to content

Instantly share code, notes, and snippets.

@dgp1130
Last active June 23, 2023 00:47
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dgp1130/ac36e63744cdeffcbc7522387662f722 to your computer and use it in GitHub Desktop.
Save dgp1130/ac36e63744cdeffcbc7522387662f722 to your computer and use it in GitHub Desktop.
Better type inference of `querySelector()` via inferred Template Literal Types
Copyright (c) 2023 Douglas Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
// Author: Doug Parker [@develwithoutacause@techhub.social](https://techhub.social/@develwithoutacause).
// ---------------------
// EXPLANATION
// ---------------------
//
// `querySelector()` uses CSS selectors to find matching DOM elements and return them. This is frequently typed as simply
// `Element` because it doesn't fully understand the selector at compile time. This makes it annoying when you want to do
// something like:
//
// const firstName = rootEl.querySelector('input#firstname')!.value;
// ^ Error: Property 'value' does not exist on type 'Element'.
//
// While `querySelector()` has an overload for simple cases like "input", it can't handle more complex queries like
// "input#foo", which is clearly an `HTMLInputElement` but will be conservatively typed as `Element` instead.
//
// This playground example implements a compile-time parser for CSS selectors using TS 4.1 inferred Template Literal Types
// (https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types).
// This is able to parse selectors and identify the tag name being looked up to provide smarter type inference. The same
// example works as expected.
//
// const firstName = querySelector(rootEl, 'input#firstname')!.value;
// ^ Compiles successfully, with `firstName` inferred to `string`.
//
// While this is definitely an "idea", I'm not yet convinced this is a *good* idea. That said, I think this is a really
// interesting example, so please play around with it and try it out! Let me know your thoughts at
// [@develwoutacause@techhub.social](https://techhub.social/@develwithoutacause)!
//
// https://www.typescriptlang.org/play?ts=4.1.0-dev.20200921#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-ChZSXn2P4VsnZSEamVhoEob5Sg-mIBOU0385z6CXIhVCtQoJEJqKeUip+KLKkzmct8xKac8WEEJXE-8l5ZGmA2ZkSpuzQD6jKvsMowjOUzJBUxORVzIVHjoB5EYdUlzqBeE0doSBnn0r6ai95eZnRCvFe+PMgLTnNO5YBUFsryVg24lDSx+QoaQjBZVKoiL+UOLRXmXZAA+EVhB9i+qZZK38lrlTWvBYs1EWLWLqpNFUbVRjdWCqQqAX1wrRVgozSGs1RKWk8sjQs65xqHUxrqXGjVia+XIpTd6gEuzc7CIXLmsRQK3AkraQAH3zbMxo3b83hrJdGt1SJeTQiTcQoZ9ahVNpNQuZ0rbGlnIuZGntcS+0Dq5a0otcqoUjLLaO8g47nJ0uTRUmdSEcWKgXLs9liA52NHZQAL1DR21dMrQDrqlXM3dtrVl9OPBmcCSMaD9V5NwDSLU2qmg9bWi9+qG1poAH7GpEW281wLf2fuLfKgoWARg6UgrA3kGc0Txs1YgSdryBX1pGQsAA1D8TheaV2kr-dG3sCsuBipI5ScjVatVnqnW87ZPq01dp7Uu9tQ6OMltUHaNQCwBMJqE8JmjXrENMuQB9CwtSmD1NYxandOG91HkkCQSAvw9PLJgfCqqJhoietIVY3gK4yj9XEEgYEFmrPwDcda6qZcILIlYORAoAC5gLEQWCbW253x0A0ukWO0LWIg3YpxEgFp1MMvrdp5Abx+zTEJTFpgOW62Ifoy2t6yACskCKyQW9LGMO9ulcBL9+bO27qSIhcB7lVZLHIAefI2XysIbE0h+g6bQBe0JcOkt1EkJQl0keqCRw6iJ0pH8zgMdaiyOo7lxDrLoRzd3QAMUTgUbbu2hptOpLSfA0H7amkKg7IRaQGqHmxTWnV430VTfo9Npl14hqofvaAJj9H9npCWwClrG62tivkbgMClc+vwAI4N0bnqwFMD1RN4H+nDMtc8PAOgWloQBDqAAHm3YWmVGb5FgLzuDcQXnIwFBx2CLIB2+m4-xy9JlTX6CEtJ+TjMVPafGfp+179xKP3AUZ6AJInnvMAZfkB6ENRFSje3hfHeIQ-B9HcI4PojhPA+F8IEYIev9cRE3uYAg5VMGfFulLPGOAUQ7lBliGUtQ0y3IOWTEqrVnvDAXN1GO-Vg9hM-ahBcVcpgzCi4uRz5gfD6R+PVpoCg5BZlAEIVR1r2h54WPiBWviPw0MIb9hsABvOcuOAC+QhbrZDynQZ8gaQQBUQvnz11OACKRDM4aWaJcW53RfX49QHE2RC5bqoGH4kAwS+axfdkUPohGae2lcdJ35ZkDYa99L-ueDDZcdb8SKP5VE+PfT5obP5p8+z+JDXyv1AYuSAU+gJL5fYUzhN9-8EhfVfVHRjBUJQB-8N9rUr8a4A9b93cp85RQAABVE2LManWRXgSXRJLAVGXganDTQg4A0A0A8AiRJRSkVbE9QqSfHbLWZXW4b4fPNENoNsd2GkBYMqUWfCB8VbJoXZWqSAROLWfKT4Y2XSaYcGJgTMdWROGPLzGBCZTEdxMFfrbvJYBYdA0vNxFoFOIaR7K4bkTEVCEwSA4guAo4BA8fJA7kDNeUaRdKKwl6OcAEMAkIFJAaJlVWTsNYJJU0anHwDNMwiAzPPAggogtZG-Ww9RbkCgAQBw5XaImwmgAoBI4wV0AAfkoAEEyJdFQGIJiLSMoF-nIE-AKQXCUGqLKP8IxDyM9ByIqN7GKLv26HyO9ByMSQxFaLsKwAyJ9DdByIoAiMCOaJoF9SqOqNGOGEIO6KYF9QaMGIKPtgYDIA6K9FQEaDWMQA2NdC2J2OMEdHMC8PkkvF8OmlkVwJCP0nMMz2wNwICNmN6KEDiKwC7VKwMASKSJmKPFSPSKWOdGGMBNAFQF+L2lTDHxKIoFqPGKmKUFqPmJBOyLqMgn+ILzeI+Lhi+ORI9CaIKV6LeL2J9GGKuJp3GMmNAGqKUAeJp3mMWOJO9FQAoDhKpOqNpKCPwAxEWOXQJNUFBBaGggQSQS+HkOGn4D8wC0-XFmzGaluhMEGIOM7EZNBNWOVMdEgLJM5PwJe3RLeJ+KeM+HRNIHpHVjn2tV+NOKaOfwtMNK8JGMNIEFVNkQoLACd1FjhA4BgxlHYFVnVjhCwCWwWHlNAAACVk95gMI3icJ-TmovSw9txMADN7YlQoIJDS4C8itBZRwgMJwCZvkcgkQogvYog8pIDfirDCTJ97CUD3SSAKzSDeiAQAQNiciiT9i8AeJ6zDSrCM10SARZsVS2zqysAVTQwuyGzt8jjTBIC6zJzr89SRykieswpxBcAJTfhChbNRxqcNjCgaQdt1zLNfhsDdzPQVyvJwAYUJkHlCCVTXQLzxAtyVkzzljnRHy-AaBXy3zXRElEBMcNNvyfz3RgD7yfRfUwL3QILBjoKPRYLnQIKPCQg6zxg+tmoVNKNNgFhHs3dE9ItIyY9Vs4AUR5BwFoI2gOgaR9DZSQ9-yZg7jcw-yAL7FKzFz78UCgY1AKN3Ugiwo1BeyPDIDOLuKRJWLITED9SOL4BFNGhhLBNEAgLwAaBuNnVoBiYLJug5LVMFKVSABxQjLRdS0jLALSyjICz0fcupUyqocyn0NAJ1ZVfYaypAWyt80C4C+Cr0Ty10TyxCjUzPSygzZyhS4Aqs9i+UNwC4aAIg5g6K4Al6X1dwwSzPeypVF1aAYKsSqEtomsiKqKmK5wnwPi3sl6PMUAsI3MfS7YwylQzStVeSrKiSpclAyK6EAqhQXi64EqvMZDJKiqz4JSlS-YIyuqrihq0Kti5AvKtqpwjqoqrq0gl6BjPqoS6ShhWS+q7Sxq2I5q6a6K2a6K+a-ixavMSTPq-q5XSfcQT8qIwDSa3Kpg5wjTF6fUdwigAABgEH8tzEfOfJgVuo13uqwGXNioBobBeiUDes+u+s+EfKvMn3uQ0kII0zCqmseo6uerzAoChq+qc3Py1Dflc3c1Z3Zx8w3KGjULGAL1tGwQSAiwjJFA-FK1LNvRmGVg9lS29wy2xDWyT0ARNgzmFLBAJguv3LSCICPL81PJRqBuXPsV6KiAABI69aiAB9JvWrZWtWpvKIVs+2OGDYwo+xaciwq6yWzc7c-IMGqoVGjOXfOGOWu68Snaj3UAHIg662pAF6ZAHG1UvfLeMAO3cIEIVA-wP8fwM+W3IOveB3MAdYfIT4X4dzIvT9CYI4YYCYboWGSCSyOyBcJYI4CvUUeWMaOgMoDkEWerUEHvGHLMW0BQdbJEFgMKdOHAH+RiYCEvLMNPMAfSqmuEWzFMSNCWOEEu8aFoWgoQIs7YzsGMmPWg-ENSKMMszPVq6K8NXo6iVueAFgRob4lAje9EigaktW6reaUAM7DVJgJ0nIy+k0SCJUsgV0tYPil3UAQMxgBYN44BBiz4I66nQ+52koyS+UI6puJyTqtQUMuQXudet6cgi6sB+rOB842Wg+t6BW0ALWjGcgIGUYfYHWt2l+64cB6KvBsfaADNVAcNGG4hqBmB0QABjBtB+UQB7Kj8bB8+8hghj8Iho66B2B6nbh0YKh0AGhgO9GsYYABWeAXCGPHw7+l4JqQuUacerPXkWyDKJPFmaoJbQ4X+yRph1BoBnKrABceyjR224G9B4x9hgYl0YEw2sR5hkxjhlWnBi+8UaEJvZWix64Hx9x8+8MtoHPbkHWocygS+iUeEj24JrkgWsx9AZga4Bkz0Zk8NPI5+vBzsfu5qD+h7IMHBAwyWRbfPSAtWULSCBQJ8YGLMCJAxnQzAthxAretuPexImxsCI+kEnIme9Yjs5p2wmEjx1kk+jxpEiJ8YjrRpxoTk7ksc5xy8ac8+aO2wEIc3AANT8FDKCDNwtxWdWesCvmWgTuAQKFHmTOghGgTrBQlkewTkgnliVnSAXERtWKxiGmgn2ggwUHBCNiaA+crpViRqXAuHyB7pOY9JCjKAyUuc+BJjtGiV5qNh4FkNebAiTuFn4GWHfmgnwBtAxHwGKnkjWFKFwGWQoiomaBMBTjIA21JnNJlUtNSVADrw2IwQlSfyw37WtUdFdEmwBC5ffXYxlT5ZdCqRgEFcHRM2AjFedBOK3AJDZ26GsmqOMCbwkZ8RcTtkpsuFhfOG0UMKiWDzUHKEYG1xj3jK+iVY4ODyOFIsgmghJu8w+fgGyy7zsy1dCXRMZfazr1AA1ZCXUGwKUH+e2O1CDbUClOAgBAXC9fUFEWMFpcQHpc+F9f2GZYGjZf5cjejaLFQHjbUEdA1aAA
// ---------------------
// TEST CASES
// ---------------------
//
// Consider copy-pasting the whole gist into TS playground (https://www.typescriptlang.org/play), where you can mouse over
// `querySelector()` to view the inferred return type. All are possibly `null` because that's the way
// `document.querySelector()` works.
const root = document.createElement('div');
querySelector(root, 'span'); // HTMLSpanElement - Handles simple string literals (though `querySelector()` already does this).
querySelector(root, 'custom-element'); // Custom - Handles custom elements added to HTMLElementTagNameMap (though `querySelector()` already does this).
querySelector(root, 'input#child'); // HTMLInputElement - Handles ID selectors.
querySelector(root, 'span.visible'); // HTMLSpanElement - Handles class selectors.
querySelector(root, 'div[active]'); // HTMLDivElement - Handles attribute selectors.
querySelector(root, 'span[foo=bar]'); // HTMLSpanElement - Handles attribute selectors.
querySelector(root, 'div#child.foo.bar'); // HTMLDivElement - Handles multiple selectors.
querySelector(root, 'input.foo#test.bar'); // HTMLInputElement
querySelector(root, 'div #parent span'); // HTMLSpanElement - Handles descendent combinator.
querySelector(root, ' div span '); // HTMLInputElement - Handles spacey descendent selectors.
querySelector(root, 'div>#parent>input'); // HTMLInputElement - Handles child combinator.
querySelector(root, 'div > #parent > input'); // HTMLInputElement - Handles spacey child combinator.
querySelector(root, 'div, span, input'); // HTMLDivElement | HTMLSpanElement | HTMLInputElement - Handles selector list.
querySelector(root, 'div , span , input'); // HTMLDivElement | HTMLSpanElement | HTMLInputElement - Handles spacey selector list.
querySelector(root, 'div.foo, div.bar, span.baz'); // HTMLDivElement | HTMLSpanElement - Handles selector list with duplicate tag names.
querySelector(root, 'div ~ span'); // HTMLSpanElement - Handles general sibling combinator.
querySelector(root, 'span + div'); // HTMLDivElement - Handles adjacent sibling combinator.
querySelector(root, 'div || input'); // HTMLInputElement - Handles column combinator.
querySelector(root, 'input:first-child'); // HTMLInputElement - Handles pseudo-class selectors.
// `querySelector()` doesn't support pseudo-elements and will always return null. We can detect this at compile-time!
querySelector(root, 'input::before'); // null
querySelector(root, 'span, input::before, div'); // HTMLSpanElement | HTMLDivElement - Still types other queries!
querySelector(root, 'div > *'); // Element - Handles universal selector by falling back to Element.
querySelector(root, '#test'); // Element - Falls back to Element when a tag name is not specified.
querySelector(root, 'div span > input#foo ~ .bar + span[active]'); // HTMLSpanElement - Put it all together!
querySelectorAll(root, 'input#child'); // NodeListOf<HTMLInputElement> - Also supports querySelectorAll().
querySelectorAll(root, 'input, div'); // NodeListOf<HTMLInputElement | HTMLDivElement> - Supports selector lists too!
// ---------------------
// IMPLEMENTATION
// ---------------------
//
// Parses the query string at compile time to extract the tag name, look up the element type, and return it.
// Type definition (implementation is just `root.querySelector{,All}(query)`).
declare function querySelector<Query extends string>(root: HTMLElement, query: Query): QueriedElement<Query> | null;
declare function querySelectorAll<Query extends string>(root: HTMLElement, query: Query): NodeListOf<QueriedElement<Query>>;
type QueriedElement<Query extends string> = Union<ElementsOf<TagNames<Selectors<Query>>>>;
// Handling selector list is tricky. Split on comma and then parse each selector individually.
// The final result of all the possible element types are then Union-ed into a single type.
type Selectors<Query extends string> = Split<Query, ','>;
// Map input over TagName<T> type.
type TagNames<Selectors extends string[]> = Selectors extends []
? []
: Selectors extends [infer Head, ...infer Tail]
? Head extends string
? Tail extends string[]
? [TagName<Head>, ...TagNames<Tail>]
: never
: never
: never
;
// Map input over ElementOf<T> type.
type ElementsOf<TagNames extends (string|null)[]> = TagNames extends []
? []
: TagNames extends [infer Head, ...infer Tail]
? Tail extends (string|null)[]
? Head extends string
? [ElementOf<Head>, ...ElementsOf<Tail>]
: [Head, ...ElementsOf<Tail>] // Head could be `null` if a pseudo-element is in the query.
: never
: never
;
type ElementOf<TagName extends string> = TagName extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TagName] : Element;
// Parse the tag name out of the given query. Returns `string` if the tag name could not be found (for cases like `.foo` or `*`).
type TagName<Query extends string> = ParseTagName<Query> extends ''
? string
: ParseTagName<Query> extends '*'
? string
: ParseTagName<Query>
;
type ParseTagName<Query extends string> = StripPseudoClasses<
CheckPseudoElements<
StripAttributes<
StripClass<
StripId<
TargetSelector<
Query
>
>
>
>
>
>;
// Parses all the combinators in a query and returns the selector that will be matched in the target.
type TargetSelector<Query extends string> = Combinators<Trim<Query>>;
type Combinators<Query extends string> = ColumnCombinator<
AdjacentSiblingCombinator<
GeneralSiblingCombinator<
ChildCombinator<
DescendentCombinator<
Query
>
>
>
>
>;
type ChildCombinator<Query extends string> = Last<Split<Query, '>'>>;
type DescendentCombinator<Query extends string> = Last<Split<Trim<Query>, ' '>>
type GeneralSiblingCombinator<Query extends string> = Last<Split<Trim<Query>, '~'>>
type AdjacentSiblingCombinator<Query extends string> = Last<Split<Trim<Query>, '+'>>
type ColumnCombinator<Query extends string> = Last<Split<Trim<Query>, '||'>>
type StripId<Selector extends string> = Split<Selector, '#'>[0];
type StripClass<Selector extends string> = Split<Selector, '.'>[0];
type StripAttributes<Selector extends string> = Split<Selector, '['>[0];
// `querySelector()` doesn't support pseudo elements (always returns `null`), detect this at compile-time by returning `null`.
type CheckPseudoElements<Selector extends string> = Selector extends `${infer _}::${infer _}`
? null
: Selector
;
type StripPseudoClasses<Selector extends string | null> = Selector extends string ? Split<Selector, ':'>[0] : null;
// ---------------------
// UTILITIES
// ---------------------
//
// These don't implement any meaningful logic, they just provide some useful functionality for writing the implementation.
// Gets the last element in the provided list (or `never` if the list is empty).
type Last<Input extends unknown[]> = Input extends [...infer _, infer Final] ? Final : never;
// Trims the given string type.
type Trim<Input extends string> = TrimLeft<TrimRight<Input>>;
type TrimLeft<Input extends string> = Input extends ` ${infer Content}` ? TrimLeft<Content> : Input;
type TrimRight<Input extends string> = Input extends `${infer Content} ` ? TrimRight<Content> : Input;
// Splits/joins the input string by the provided delimiter recursively.
type Split<Input extends string, Delim extends string> = Input extends []
? []
: Input extends `${infer First}${Delim}${infer Remaining}`
? [First, ...Split<Remaining, Delim>]
: [Input]
;
// Converts the given array into a union type of all its components.
type Union<Input extends unknown[]> = Input extends []
? never
: Input extends [infer Head, ...infer Tail]
? Head | Union<Tail>
: Input
;
// ---------------------
// ENVIRONMENT
// ---------------------
//
// These types would be present in a real project, but need to be explicitly defined to use as test cases.
// These types should be included by default, but don't seem to be available in TS Playground.
interface HTMLElementTagNameMap {
'span': HTMLSpanElement;
'div': HTMLDivElement;
'input': HTMLInputElement;
// More mappings...
}
// Custom elements should also add themselves to the tag name map and they will be supported too!
class Custom extends HTMLElement { }
customElements.define('custom-element', Custom);
interface HTMLElementTagNameMap {
'custom-element': Custom;
}
@jellelicht
Copy link

This is some cool stuff; any chance you can slap a license on it? Thanks in advance! (Even if it ends up being an All Rights Reserved kinda thing).

@dgp1130
Copy link
Author

dgp1130 commented Jun 23, 2023

@jellelicht I threw an MIT license on it if you really want to do anything with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment