There's no guarantee that the selector will match any element, in which case it returns
null. If you know from context that such an element does exist, you can use a non-
null assertion (
!) to silence the error:
codeBlockEl! is shorthand for
(codeBlockEl as Element) and is just as unsafe as any type assertion. If you don't know that the selector will match, you can use a conditional to narrow its type:
You can also use the optional chaining operator to allow
undefineds to bubble up:
null isn't the only problem. You might also get a type that's not specific enough. For example:
document.querySelector returns an
Element, but the
value property is only defined on
HTMLInputElement, which is a subtype. The solutions to this problem are similar. You can either use an unsafe type assertion:
Or you can use an
instanceof check to narrow the type:
If you plug this last example into the TypeScript playground and mouse over
nameEl on the last line, you'll see that its TypeScript type is
HTMLInputElement. Every other type would have made the code throw.
Writing these sorts of checks out every time you use
querySelector quickly becomes tedious. Let's try to write a generic version instead!
null checking is easy to abstract:
You can swap this in for any use of
querySelector to exclude
But what about the case where you need a more specific type? It's tempting to plug in a generic type parameter and try to implement something like this:
The error on the
instanceof check is a fundamental one that cannot be worked around.
instanceof is an operator that is evaluated at runtime. As Item 3 of Effective TypeScript ("Understand That Code Generation Is Independent of Types") and many other sites explain, TypeScript types do not exist at runtime. So referencing them in a runtime expression is nonsensical. You can't get a value at runtime from a TypeScript type. It just won't work.
You can go the other way, however! Given a value, you can get a TypeScript type using
typeof. And fortunately for us, there are runtime values corresponding to all the DOM types, just as there are for all classes.
HTMLButtonElement in TypeScript can refer to either a type or a value depending on the context.
So instead of passing in the type we want, let's pass in a value. Here's a sketch:
Don't let the name fool you:
type here is a value. We've got a few blanks to fill in, but this declaration at least has the great advantage of not going against TypeScript's design principles!
Every value has a type. So what's the type of
HTMLButtonElement in the previous function call? It's
typeof HTMLButtonElement, of course! You can see what this really is by hovering over
T in the following sample:
This definition comes from
lib.dom.d.ts. It says that you can
new an instance of
typeof HTMLButtonElement to get an instance of the
HTMLButtonElement type. The other DOM element classes (such as
HTMLElement) have similar types.
We can use this to place a constraint on the generic parameter,
T extends typeof Element refers to a value, but
typeof Element is a type. If
HTMLButtonElement is a subtype of
typeof HTMLButtonElement is a subtype of
typeof Element. Passing in
HTMLButtonElement will work, but passing in
console won't. And by using a generic parameter, we'll be able to infer a precise type based on the value of the
type parameter at the call site.
And this works exactly as you'd hope:
(If you know how to get rid of the type assertion on the
return line, please let me know!)
If you preferred the syntax of the explicit generic argument, where the
Element subtype is more clearly separated from the usual
querySelector parameters, you can get close by currying:
typeof), but not from a type to a value. So the solution is often to rework your API to pass around values and derive the types from those. The safe
querySelector we've built in this post is one example of doing that. In future posts we'll look at a few more examples of how this strategy can play out in practice.
- Item 3: "Understand That Code Generation Is Independent of Types" or Evan Martin's TypeScript's type independent output.
- Item 8: "Know How to Tell Whether a Symbol Is in the Type Space or Value Space" or Florian Reuschel's TypeScript's Secret Parallel Universe
- Item 55: "Understand the DOM hierarchy"