Custom Typescript JSX Custom Typescript JSXtypescript jsx tsx

This post explores how to configure TypeScript for custom JSX handling.

Why 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.

Enabling Custom JSX

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'
	}
}

Defining a Custom createElement Function

This 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.

Handling Different Element Types

The function should be able to handle different element types encountered in your JSX, including:

Intrinsic Element Map and Typing

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.

JSX Element Attributes and Children Typing

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} />;

Import your createElement function

To 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".

JSX Fragments

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.

React-Specific JSX Types

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
		}
	}
}
Back to Main Page