This post explores how to configure TypeScript for custom JSX handling.
While popular libraries like React use JSX to build user interfaces, there may be situations where you want a more specialized approach. Custom JSX handling allows you to define your own element syntax, data binding mechanisms, and rendering logic.
The tsconfig.json
file controls TypeScript configurations. To enable custom JSX and specify a custom factory function, add the following options:
{
...
"compilerOptions": {
...
// Enable JSX parsing
"jsx": 'react',
// Use our custom "dom" function for element creation
"jsxFactory": 'dom'
// Handle <></> tag (optional)
"jsxFragmentFactory": 'fragment'
}
}
createElement
FunctionThis function allows you to tailor element creation to your specific needs. By defining a custom createElement
function, you gain control over how JSX elements are transformed into the actual DOM structure.
type Element = HTMLElement;
type FunctionElement<T> = (attributes?: T) => Element | Element[]
type ClassElement = any;
export function dom(
tagName: string | FunctionElement | ClassElement,
attributes?: Attributes,
...children: (string | Element)[]
): Element { }
Text nodes will be passed to the function as strings. When using fragments <>
, the first argument will be your createElement
function, and the second argument will be undefined
. The remaining arguments will be the child elements of the fragment.
The function should be able to handle different element types encountered in your JSX, including:
An Intrinsic Element Map in TypeScript defines types for built-in HTML elements like div
, span
, etc. While most types are included in dom.d.ts
, you can create your own map for better type checking:
declare global {
namespace JSX {
export interface IntrinsicElements {
div: HTMLDivElement;
}
}
}
// Now you can use the div tag name in jsx
<div title="attribute" />;
The HTMLElementTagNameMap
type contains a map of all html elements and their attributes, and can be used to define intrinsic elements.
type IntrinsicElements = {
[P in keyof HTMLElementTagNameMap]: NativeType<HTMLElementTagNameMap[P]>;
};
A similar type map also exists for Svg elements, SVGElementTagNameMap
.
When using class elements, uppercase tag names, the ElementAttributesProperty
and ElementChildrenAttribute
types tell the typescript compiler what property to use when type checking the JSX element attributes and children respectively.
declare global {
namespace JSX {
interface ElementAttributesProperty {
jsxAttributes: any;
}
interface ElementChildrenAttribute {
children: {};
}
}
}
class ClassElement {
jsxAttributes: { name: string; optional?: number };
}
<ClassElement name="attribute" optional={10} />;
createElement
functionTo use your custom createElement
function in your source files, you need to import it from the file where it is defined. For example:
import { dom } from './custom-jsx';
const el = <CustomJSX>Hello</CustomJSX>;
Here, you import the dom
function (assuming it is named dom) from the ./custom-jsx
module and use it to create a CustomJSX element with the text "Hello".
Since TypeScript 4, you can also define a fragment
function to handle JSX fragments. This function will be used to replace the <>
and </>
tags in your JSX code.
import { dom, fragment } from './custom-jsx';
const el = (
<>
<div />
<span />
</>
);
The fragment function should have the following signature:
function fragment(
/* your defined createElement function */
createElement: typeof dom,
attrs: undefined,
...children: Element[]
): Element | DocumentFragment;
The fragment function takes your createElement
function as the first argument, an undefined
value for attributes (since fragments don't have attributes), and any child elements as the remaining arguments. It should return a document fragment or a single element containing the child elements.
By using the React namespace, you can define custom JSX elements that are specifically designed for use within the React framework.
declare global {
namespace React {
namespace JSX {
// Define your custom element types here
}
}
}