Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript JSX without React

Tags:

typescript

jsx

I'd like to use JSX syntax in TypeScript, but I don't want to use React.

I have seen answers to other related questions here on SO, but nothing was complete or detailed enough to be of any help. I've read this guide and the JSX chapter of the handbook, and it didn't help much. I don't understand how the language-feature itself really works.

I've tried to examine the React declarations, but it's just too huge for me to absorb - I need a minimal, working example demonstrating type-checked elements, components and attributes. (It doesn't need to have a working implementation of the factory-function, I'm mostly interested in the declarations.)

What I'd like, minimally, is an example that makes the following work, with type-safety:

var el = <Ping blurt="ya"></Ping>;

var div = <div id="foo">Hello JSX! {el}</div>;

I assume I'll need to declare JSX.IntrinsicElements at least, and a createElement() factory-function of some sort, but that's about as far as I got:

declare module JSX {
    interface IntrinsicElements {
        div: flurp.VNode<HTMLDivElement>;
    }
}

module flurp {

    export interface VNode<E extends Element> {
        id: string
    }

    export function createElement<T extends Element>(type: string, props?: any, ...children: (VNode<Element>|string)[]): VNode<Element> {
        return {
            id: props["id"]
        };
    }
}

class Ping {
    // ???
}

var el = <Ping blurt="ya"></Ping>;

var div = <div id="foo">Hello JSX! {el}</div>;

I compile this with tsconfig.json like:

{
    "compilerOptions": {
        "target": "es5",
        "jsx": "react",
        "reactNamespace": "flurp"
    }
}

Hovering over the <div> element and id="foo" attribute, I can see the compiler understands my declaration of intrinsic elements - so far, so good.

Now, to get the <Ping blurt="ya"> declaration to compile, with type-checking for the blurt attribute, what do I do? Should Ping even be a class or is it maybe a function or something?

Ideally, elements and components should have a common ancestor of some sort, so I can have a common set of attributes that apply to both.

I'm looking to create something simple and lightweight, perhaps along the lines of monkberry, but with a JSX syntax, e.g. using components like <If> and <For> rather than inline statements. Is that even feasible? (Is there any existing project along those lines I can look at for reference?)

Please, if anyone can provide a working, minimal example of how to use the language feature itself, that would be a huge help!

like image 813
mindplay.dk Avatar asked Oct 18 '22 19:10

mindplay.dk


1 Answers

Indeed, the JSX section of the TypeScript handbook is a great resource; for those of you who haven't, check it out.

If you want tsc to compile your JSX, you will need to do the following (extremely broad overview):

  1. Declare the IntrinsicElements interface/type in the JSX namespace (enable type-safety).
  2. Create a JSX factory function.
  3. Configure your consuming app's tsconfig to use "react" code generation and point to your JSX Factory (using the "jsxFactory" option).
  4. Import your JSX factory function into a .tsx file.

Here's a more in-depth look at this. Also, you might like to see a repository or maybe a working example/playground.

The JSX namespace

Here, you tell TypeScript how to interpret your JSX code. At a minimum, you need to declare IntrinsicElements, but there are other types you can declare that will give you better type-hinting, enable component features, and generally improve/tweak how your JSX is understood by TypeScript.

Here's an example declaration of the JSX namespace:

/// <reference lib="DOM" />

declare namespace JSX {
    // The return type of our JSX Factory: this could be anything
    type Element = HTMLElement;

    // IntrinsicElementMap grabs all the standard HTML tags in the TS DOM lib.
    interface IntrinsicElements extends IntrinsicElementMap { }


    // The following are custom types, not part of TS's known JSX namespace:
    type IntrinsicElementMap = {
        [K in keyof HTMLElementTagNameMap]: {
            [k: string]: any
        }
    }

    interface Component {
        (properties?: { [key: string]: any }, children?: Node[]): Node
    }
}

A few notes:

  • If your intending to work with HTMLElements and Nodes, the DOM lib (lib.dom.d.ts) is a great resource to get better type checking. In this example, I've used it to declare all real HTML tags as valid Intrinsic elements. You could further extend this, for example, to preload EventHandlers like onclick via GlobalEventHandlers.
  • Element is another type TypeScript looks for in the JSX namespace. This is optional (defaulting to any). Use this to specify the return type of your factory function.
  • We can create a simple function-based component with this definition (I've defined a custom helper with the Component interface.
  • For class components, we need to declare the JSX.ElementClass interface. See here for more details.

The JSX Factory Function

The JSX namespace helps us define how we intend to use JSX in our code, but we still need to implement a function that can handle the code generated by tsc. Our factory function should follow the form (tag: string, properties: { [k: string]: any }, ...children: any[]): any, or something more specific. Here's an example that will enable functional components (N.B. it's big and ugly):

function jsx(tag: JSX.Tag | JSX.Component, 
             attributes: { [key: string]: any } | null, 
             ...children: Node[]) 
{

    if (typeof tag === 'function') {
        return tag(attributes ?? {}, children);
    }
    type Tag = typeof tag;
    const element: HTMLElementTagNameMap[Tag] = document.createElement(tag);

    // Assign attributes:
    let map = (attributes ?? {});
    let prop: keyof typeof map;
    for (prop of (Object.keys(map) as any)) {

        // Extract values:
        prop = prop.toString();
        const value = map[prop] as any;
        const anyReference = element as any;
        if (typeof anyReference[prop] === 'undefined') {
            // As a fallback, attempt to set an attribute:
            element.setAttribute(prop, value);
        } else {
            anyReference[prop] = value;
        }
    }

    // append children
    for (let child of children) {
        if (typeof child === 'string') {
            element.innerText += child;
            continue;
        }
        if (Array.isArray(child)) {
            element.append(...child);
            continue;
        }
        element.appendChild(child);
    }
    return element;

}

TSX and tsconfig

Now, all that's left is to use our jsx function. Our tsconfig should configure at least the following:

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "jsx",
  }
}

Now, we can write JSX in any .tsx file as long as we import our jsxFactory:

import jsx from './jsxFactory';

function Ping({ blurt }: { blurt: string }) {
    return <div>{blurt}</div>
}

var el = <Ping blurt="ya"></Ping>;

// var div: HTMLElement
var div = <div id="foo">Hello JSX! {el}</div>;

document.body.appendChild(div)

Thanks to JSX.Element, TypeScript knows the result of JSX is an HTMLElement.

like image 119
Connor Low Avatar answered Oct 27 '22 11:10

Connor Low