In Part I of this series, we learned basic JSX syntax and some basic terminology when discussing the abstract syntax tree of JSX expressions. Let's now dive into how TypeScript checks the type validity of JSX expressions.
Not everything can be valid JSX constructors, you can't just shove any random value into the opening tag of a JSX expression:
// bad! it's actually 'a'
const badAnchor = <anchor href='dev.to'>Go to dev.to!</anchor>
// bad! it's not a function!
const MyComponent = {}
const badFunctionElement = <MyComponent>Hi!</MyComponent>
// bad! it's not something that can render!
class MyClassComponent {
constructor(props: any) { this.props = props }
}
const badClassElement = <MyClassComponent>Hi!</MyClassComponent>
So how does TypeScript know when something is a valid JSX element constructor? The answer lies in the magical JSX
namespace. Remembering how the jsxFactory
compiler option (or the @jsx
pragma) works, we have that the factory function for React is React.createElement
. You might also be using some other library, where the factory function is often called h
:
// @jsx React.createElement
import React from 'react'
// @jsx h
import { h } from 'preact'
TypeScript will attempt to look up a namespace called JSX
under the factory function and fallback to a global one if none is found:
- for factory functions that are under another namespace, like
React.createElement
, it will look forReact.JSX
- for factory functions that are just a naked identifier, like
h
, it will look forh.JSX
- if no
JSX
namespace is found, it looks for a globalJSX
namespace
The React type definitions declares a global JSX
namespace, though that's not a good idea and we should change that soon 😅.
So what's the use of the JSX
namespace? TypeScript looks for specific interfaces under it to figure out what's acceptable for each type of JSX element constructor:
- for "intrinsic" element constructors (lower-case tag name), it looks if a property with that same key exists under
JSX.IntrinsicElements
. - for function element constructors, it checks if its return type is assignable to the
JSX.Element
interface. - for class-based element constructors, it checks if its instance type is assignable to the
JSX.ElementClass
interface.
Let's look at each case in detail:
Intrinsic Element Constructors
If your JSX namespace looks like this:
namespace JSX {
interface IntrinsicElements {
a: HTMLAttributes<HTMLAnchorElement>
button: HTMLAttributes<HTMLButtonElement>
div: HTMLAttributes<HTMLElement>
span: HTMLAttributes<HTMLElement>
}
}
Then you can render these elements:
const validIntrinsicElements = [<a />, <button />, <div />, <span />]
// error properties 'select', 'main', and 'nav' do not exist on type 'JSX.IntrinsicElements'
const invalidIntrinsicElements = [<select />, <main />, <nav />]
We'll talk about what the types of the properties themselves actually mean in the next part of the series.
Function Element Constructors
If your JSX namespace looks like this:
namespace JSX {
interface Element {
key?: string
type: string | (() => any)
props: { [propName: string]: any }
}
}
And you have a function like this:
function MyComponent(props: any) {
return {
type: MyComponent,
props: props
}
}
Then you have a valid constructor! Because its return type is assignable to JSX.Element
:
const myFunctionElement = <MyComponent /> // good to go!
How is it though, that when you have a function without its return type annotated, but it returns JSX, it's still okay? That's because TypeScript will treat any JSX expression's type to be the same type as JSX.Element
!
function MyComponent() {
return <div>Hi!</div>
}
const myFunctionElement = <MyComponent /> // still okay
const nakedElement = <div>hi!</div>
type NakedElementType = typeof nakedElement // the type is JSX.Element
An astute reader will notice that this has some odd pitfalls when it comes to what React allows you to return from a component. Remember that React allows you to return arrays, strings, numbers, and booleans from a component, which it will happily render:
function MyStringFragment() {
return ['a', 'b', 'c'] // type is string[]
}
const myFragment = <MyStringFragment /> // TS error!
Uh oh, this is an unfortunate limitation of the type checker; if we want to get the check to pass, we need to assert the type of the return value:
function MyStringFragment() {
return ['a', 'b', 'c'] as any as JSX.Element
}
const myFragment = <MyStringFragment /> // good now!
There is an open issue for the TypeScript repo that will hopefully resolve this issue in the future: https://github.com/Microsoft/TypeScript/issues/14729.
Class Element Constructors
If your JSX namespace looks like this:
namespace JSX {
interface ElementClass {
render(): any
}
}
And you have a class like this:
class Component {
constructor(props: any) {
this.props = props
}
render() {
return { obviouslyNotAnElement: 'fooled ya!' }
}
someOtherMethod(): string
}
Then you have a valid constructor! Because its instance type is assignable to JSX.ElementClass
:
const myComponentInstance = new Component({})
type myComponentInstanceType = {
render(): { obviouslyNotAnElement: string }
someOtherMethod(): string
}
type ComponentInstanceType = {
render(): any
}
Obviously the real React type is different, but that's why we always extend
from React.Component
, because this is what it roughly looks like in React's types:
namespace React {
type Renderable = JSX.Element | JSX.Element[] | number | string | boolean | null
class Component {
/* other methods like setState, componentDidUpdate, componentDidMount, etc */
render(): Renderable
}
namespace JSX {
interface ElementClass {
render(): Renderable
}
}
}
And now any class that you declare that extends React.Component
will be a valid constructor!
In summary: before we even talk about props, TypeScript has to check if a component is actually a valid JSX constructor, otherwise it rejects it when you try to use it in a JSX expression.
In the next post in this series, we'll be talking about what TypeScript considers to be valid attributes given a specific JSX expression (remember: attributes are the props you give to a JSX expression, like HTML element attributes).
Top comments (3)
Is there some way to make a JSX expression like
<div></div>
have a type other thanJSX.Element
without casting it?I'm wondering, because with Solid (one of the fastest view libs), which uses JSX for convenient syntax for creating DOM with reactive expressions, the type of
div
inin plain JavaScript is actually
HTMLDivElement
;div
is a reference to an actual<div>
element after the assignment.I'm wondering if there's a way to make that typing possible, without having to write something like
Hello,
Where is HTMLAttributes coming from? TSC is saying it does not exist.
No worries, worked it out :)
It's the interface that defines the valid attribute/props for a JSX element.