TypeScript JSX utan React


Jag skulle vilja använda JSX-syntax i TypeScript, men jag vill inte använda React.

Jag har sett svar på andra relaterade frågor här på SO, men inget var komplett eller tillräckligt detaljerat för att vara till någon hjälp. Jag har läst denna guide och JSX-kapitlet i handboken, och det hjälpte inte mycket. Jag förstår inte riktigt hur själva språkfunktionen fungerar.

Jag har försökt att granska React-deklarationerna, men det är helt enkelt för stort för mig att ta in - jag behöver ett minimalt, fungerande exempel som visar typkontrollerade element, komponenter och attribut. (Det behöver inte ha en fungerande implementation av fabrikfunktionen, jag är mest intresserad av deklarationerna.)

Det jag skulle vilja ha, som ett minimum, är ett exempel som får följande att fungera, med typ-säkerhet:

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

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

Jag antar att jag åtminstone behöver deklarera JSX.IntrinsicElements och en createElement()-fabriksfunktion av något slag, men det är ungefär så långt jag kommit:

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

Jag kompilerar detta med tsconfig.json så här:

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

När jag hovrar över <div>-elementet och id="foo"-attributet kan jag se att kompilatorn förstår min deklaration av intrinsiska element - hittills, så bra.

Nu, för att få <Ping blurt="ya">-deklarationen att kompilera, med typkontroll för blurt-attributet, vad gör jag? Bör Ping ens vara en klass eller är det kanske en funktion eller något?

Idealiskt sett bör element och komponenter ha en gemensam förfader av något slag, så att jag kan ha en gemensam uppsättning attribut som gäller för båda.

Jag vill skapa något enkelt och lättviktigt, kanske i stil med monkberry, men med en JSX-syntax, t.ex. använda komponenter som <If> och <For> istället för inline-uttryck. Är det ens genomförbart? (Finns det något befintligt projekt i den stilen som jag kan titta på som referens?)

Snälla, om någon kan ge ett fungerande, minimalt exempel på hur man använder språkfunktionen själv, skulle det vara till stor hjälp!

Svar #1

Faktiskt är JSX-avsnittet i TypeScript-handboken en utmärkt resurs; för er som inte har gjort det, kolla in det.

Om du vill att tsc ska kompilera din JSX, behöver du göra följande (mycket översiktlig översikt):

  1. Deklarera IntrinsicElements-gränssnittet/typen i JSX-namnrymden (aktivera typer-säkerhet).
  2. Skapa en JSX-fabrikfunktion.
  3. Konfigurera din konsumerande apps tsconfig för att använda "react" (eller ett annat jsx-alternativ) kodgenerering och peka på din JSX-fabrik (med hjälp av "jsxFactory"-alternativet).
  4. Importera din JSX-fabrikfunktion till en .tsx-fil.

Här är en mer djupgående titt på detta. Du kanske också vill se ett arkiv eller kanske ett fungerande exempel/lekplats.

JSX-namnområdet

Här talar du om för TypeScript hur det ska tolka din JSX-kod. Minst måste du deklarera IntrinsicElements, men det finns andra typer du kan deklarera som ger dig bättre typförslag, aktiverar komponentfunktioner och generellt förbättrar/justerar hur din JSX förstås av TypeScript.

Här är ett exempel på deklaration av JSX-namnområdet:

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

Några anteckningar:

  • Om du avser att arbeta med HTMLElement- och Node-objekt, är DOM-biblioteket (lib.dom.d.ts) en utmärkt resurs för bättre typkontroll. I det här exemplet har jag använt det för att deklarera alla riktiga HTML-taggar som giltiga Intrinsic-element. Du kan vidareutveckla detta, till exempel för att förladda EventHandler-objekt som onclick via GlobalEventHandlers.
  • Element är en annan typ som TypeScript letar efter i JSX-namnutrymmet. Detta är valfritt (standard är any). Använd detta för att specificera returtypen för din fabriksfunktion.
  • Vi kan skapa en enkel funktionsbaserad komponent med denna definition (jag har definierat en anpassad hjälpare med Component-gränssnittet.
  • För klasskomponenter behöver vi deklarera JSX.ElementClass-gränssnittet. Se här för fler detaljer.

JSX-fabrikfunktionen

Namespace JSX hjälper oss att definiera hur vi avser att använda JSX i vår kod, men vi behöver fortfarande implementera en funktion som kan hantera den kod som genereras av tsc. Vår fabrikationsfunktion bör följa formen (tag: string, properties: { [k: string]: any }, ...children: any[]): any, eller något mer specifikt. Här är ett exempel som gör det möjligt med funktionella komponenter (Obs! Den är stor och ful):

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 och tsconfig

Nu återstår bara att använda vår jsx-funktion. Vår tsconfig bör åtminstone konfigurera följande:

{
  "compilerOptions": {
    "jsx": "react", // or react-jsx, etc.
    "jsxFactory": "jsx",
  }
}

Nu kan vi skriva JSX i vilken .tsx-fil som helst så länge vi importerar vår 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)

Tack vare JSX.Element vet TypeScript att resultatet av JSX är ett HTMLElement.

Relaterade artiklar