JSX (Full)

https://jsx.github.io/doc/tutorial.html

Đọc thêm:

https://www.typescriptlang.org/docs/handbook/jsx.html

Background

JSX is a statically-typed, object-oriented programming language compiling to standalone JavaScript. The reason why JSX was developed is our need for a more robust programming language than JavaScript. JSX is, however, fairly close to JavaScript especially in its statements and expressions.

Statically-typed programming language is robust because certain sorts of problems, for example typos in variable names or missing function definitions, are detected at compile-time. This is important especially in middle- to large-scale software development in which a number of engineers may be engaged.

Therefore, JSX is designed as a statically-typed language. All the values and variables have a static type and you can only assign a correctly-typed value to a variable. In addition, all the functions including closures have types which are determined by the types of parameters and the return values, where you cannot call a function with incorrectly typed arguments.

Also, another important reason why JSX was developed is to boost JavaScript performance. JavaScript itself is not so slow but large-scale development tends to have many abstraction layers, e.g. proxy classes and accessor methods, which often have negative impact on performance. JSX boosts performance by inline expansion: function bodies are expanded to where they are being called, if the functions being called could be determined at compile-time. This is the power of the statically-typed language in terms of performance.

Run "Hello, World!"

Let's start by running our first JSX program: hello.jsx. We use the jsx command, which is the JSX compiler in the JSX distribution, to compile JSX source code to JavaScript.

First, install jsx with npm:

$ npm install -g jsx

Then, make the code below as hello.jsx:

class _Main {
    static function main(args : string[]) : void {
        log "Hello, world!";
    }
}

Now you can run a JSX program with the following command and you will get Hello, world on the console.

$ jsx --run hello.jsx

We will look into the detail of hello.jsx in the next section.

Program Structure

Here is hello.jsx, the source code of the "Hello world!" example. You can see several features of JSX in this program, namely, static types and class structure within the source code.

class _Main {
    static function main(args : string[]) : void {
        log "Hello, world!";
    }
}

Class _Main has a static member function (a.k.a. a class method) named main, that takes an array of strings and returns nothing. _Main.main(:string[]):void is the entry point of JSX applications that is called when a user invokes an application from command line. JSX, like Java, does not allow top-level statements or functions.

The log statement is mapped to console.log() in JavaScript, which displays the arguments to stdout with a newline.

Next, we look into another typical library class, Point:

class Point {
    var x = 0;
    var y = 0;

    function constructor() {
    }

    function constructor(x : number, y : number) {
        this.set(x, y);
    }

    function constructor(other : Point) {
        this.set(other);
    }

    function set(x : number, y : number) : void {
        this.x = x;
        this.y = y;
    }

    function set(other : Point) : void {
        this.set(other.x, other.y);
    }
}

As you can see, member variables of Point, var x and var y, are declared without types, but their types are deducted from their initial values to be number.

You might be surprised at multiple definition of constructors: one takes no parameters and the others take parameters. They are overloaded by their types of parameters. When you construct the class with new Point(), the first constructor, which takes no parameters, is called. The second with two parameters will be called on new Point(2, 3) and the third with one parameter will be called as a copy constructor. Other forms of construction, e.g. new Point(42) or new Point("foo", "bar") will cause compilation errors of mismatching signatures. The Point#set() functions are also overloaded and the compiler know how to call the correct one.

Static Typing

Basic type concept will be described in this section. Primitive types, object types, variant type, and Nullable types exist in JSX.

Pritimive Types

There are three pritimive types in JSX: string, number, and boolean. The three are non-nullable, immutable types. The code snippet below declares three variables s, n, b with their repective types, annocated to the right of the name of the variables using the : mark.

var s : string;
var n : number;
var b : boolean;

Type annotations can be omitted when a variable is initialized at the same moment of declaration.

var s = "hello";  // s is string, initialized as "hello"
var n = 42;       // n is number, initialized as 42
var b = true;     // b is boolean, initialized as true

Object Types

Object types are types of values to hold reference to objects - which are instances of classes. For example, functions, string[] (array of strings), Date are all object types. Whether they are mutable or not depends on the definition of each class.

Most of the objects (values of object types) are constructed using the new operator.

var d = new Date();            // instantiate an object of class Date
var a = new Array.<string>();  // instantiate an array of string
var m = new Map.<number>();    // instantiate an associative map of strings to numbers

Array and Map types can also be instatiated by using their initializer expressions.

var a1 = [] : Array.<string>;  // a1 is Array.<string>, and is empty
var a2 = [ 1, 2, 3 ];          // a2 is Array.<number> with three elements

var m1 : {} : Map.<number>;    // m1 is Map.<number>
var m2 = {                     // m2 is Map.<string>
  en: "Good morning",
  de: "Guten Morgen",
  ja: "おはようございます"
};

Variables of the Function class can only be instantiated as a static function or by using function expression or function statement (the details are described laterwards).

The Variant Type

Variant type, which means "no static type information," is useful for interacting with existing JavaScript APIs. Some JavaScript libraries may return a variant value, which type cannot be determined at compile time. All you can do on variant values is to check equality of a variant value to another variant value. You have to cast it to another type before doing anything else on the value.

function guessTheType(v : variant) : void {
    if (typeof v == "string") {
        log "v is string and the value is:" + v as string;
    } else {
        log "v is not string";
    }
}

Nullable Types

Nullable type is a meta type which indicates a value may be null. It is prohibited to assign null to the primitive types (note: Object types are nullable by default). Nullable types should instead be used for such purposes.

var s1 : string;
s1 = "abc";       // ok
s1 = null;        // compile error!  cannot assign null to string

var s2 : Nullable.<string>;
s2 = "abc";       // ok
s2 = null;        // ok

The most prominent use case of Nullable types is when interacting with an array. For example, an out-of-bounds access to an array returns null.

var a = [ 1, 2, 3 ]; // creates Array.<number> with three elements
a[3];                // out-of-bounds access, returns null

There are APIs that return Nullable types also exists. For example, the return type of Array.<string>#shift() is Nullable.<string>. When you use a Nullable value, you have to make sure of the value is not null.

function shiftOrReturnEmptyString(args : string[]) : string {
    if (args.length > 0)
        return args.shift();
    else
        return "";
}

When the source code is compiled in debug mode (which is the default), the compiler will insert run-time type-checking code. An exception will be raised (or the debugger will be activated) when misuse of a null value as actual value is detected. Run-time type checks can be omitted by compiling the source code with the --release option.

Please refer to the Types section of the language reference for more information.

Expressions

The definitions of operators in JSX are almost equivalent to JavaScript, however there are few exceptions.

  • arithmetic operators (+, -, *, /...) only accept numbers as the operands

    var a = 3;
    a + 1;      // OK, returns 4
    a * a;      // OK, returns 9
    a + "abc";  // compile error

    Note: concatenation operator + exists for concatenation of strings

  • the dot property accessor can only access the defined properties

    class Point {
        var x : number;
        var y : number;
        function print() : void {
            log this.z;             // compile error! no property named z
        }
    }
  • the [] property accessor can only be applied to values of type Map or variant

    var m = {            // m is Map.<string>
        hello: "world!"
    };
    log m["hello"];      // OK
    log m.hello;         // compile error!
  • introduction of the as operator, used for type conversion between primitive types / casting object types

    var n = 123;
    var s = "value of n is " + (n as string);
    log s;               // print "value of n is 123"
  • logical operators (&&, ||) returns boolean, and the introduction of binary ?: operator as the equivalent to the || operator of JavaScript

A complete list of operators can be found in the Operator Reference.

Statements

JSX supports most of the statement types provided by JavaScript. The exceptions are:

  • log statement

    log "hello, world";    // log strings to console, can turned off with compile option: --release
  • assert statement

    var n = 123;
    assert n != 0;         // assertions.  also can be turned off with --release
  • try-catch-finally statement

    try {
        ...
    } catch (e : TypeError) {
        // got TypeError
    } catch (e : Error) {
        // got Error, which is not TypeError
    } catch (e : variant) {
        // applications may throw any kind of value
    } finally {
        ...
    }
  • no with statement

A complete list of statements can be found in the Statement Reference.

Classes and Interfaces

JSX is a class-based object-oriented language, and its class model is similar to Java.

  • a class may extend another class (single inheritance)

  • a class may implement multiple interfaces and mixins

  • all classes share a single root class: the Object class

interface Flyable {
    abstract function fly() : void;
}

abstract class Animal {
    function eat() : void {
      log "An animal is eating!";
    }
}

class Bat extends Animal implements Flyable {
    override function fly() : void {
        log "A bat is flying!";
    }
}

abstract class Insect {
}

class Bee extends Insect implements Flyable {
    override function fly() : void {
        log "A bee is flying!";
    }
}

class _Main {

    static function main(args : string[]) : void {
        // fo bar
        var bat = new Bat();

        var animal : Animal = bat; // OK. A bat is an animal.
        animal.eat();

        var flyable : Flyable = bat; // OK. A bat can fly
        flyable.fly();

        // for Bee
        var bee = new Bee();

        flyable = bee; // A bee is also flyable
        flyable.fly();
    }
}

In the example, the Bat class extends the Animal class, so it inherits the Animal#eat() member function, and it can be assigned to a variable typed to Animal. The class also implements the Flyable interface overriding the Flyable#fly() member function, so it can be assigned to a variable typed Flyable. There's also another flyable class, Bee. By using the Flyable interface, it is possible to deal with both classes as a flyable being, even if the organ of a bee is completely different from that of a bat.When overriding a member function, the use the override keyword is mandatory. Otherwise the compiler will report an error. In other words, you are saved from unexpected interface changes in the base classes which cause compilation errors in derived classes instead of undesirable runtime errors.

Functions and Closures

In JSX, functions are first-class objects and they have static types. You can declare a variable of a function type like var f : function(arg : number) : number, a function that takes a number as an argument and returns another number (or, just returns the same value as the argument; but it's not important here). The variable f can be called as f(42) from which you will get a number value.It is possible to define closures using the function expression or the function statement. They are typically used to implement callbacks ore event listeners which are popular in GUI programming. Closures are similar to JavaScript except for what this points at: when a closure is defined within a member function, it refers to the receiver of the member function. See the following example.

class _Main {
    var foo = 42;

    function constructor() {
        var f = function() : void {
            log this.foo;
        };

        f(); // says 42
    }

    static function main(args : string[]) : void {
        var o = new _Main();
    }
}

Type annocations of function expressions / statements may be omitted if they can be inferred by the compiler. In the exmaple below, both the type of the argument n and the return type of the function expression is inferred from the definition of Array#map to be number.

var doubled = [ 1, 2, 3 ].map(function (n) {
    return n * 2;
});

Modules

JSX has a module system. You can use JSX class libraries by the import statement. For example, the following program uses timer.jsx module, which exports the Timer class.

import "timer.jsx";

class _Main {

    static function main(args : string[]) : void {
        Timer.setTimeout(function() : void {
            log "Hello, world!";
        }, 1000);
    }

}

A module may export multiple classes, but you can specify what modules you import or name a namespace which the module is imported into.

Interface to Web Browsers

The js/web.jsx module provides the interface to web browser APIs, e.g. the window object and DOM APIs. The example below shows how to insert a text node into an HTML.

// hello.jsx
import "js/web.jsx";

class _Main {

    static function main(args : string[]) : void {
        var document = dom.window.document;

        var text = document.createTextNode("Hello, world!");
        document.getElementById("hello").appendChild(text);
    }

}
<!DOCTYPE html>
<html>
  <head>
    <title>Hello, world!</title>
    <script src="hello.jsx.js"></script>
  </head>
  <body>
  <p id="hello"></p>
  </body>
</html>

Once you compile hello.jsx by the following command, then you can access the HTML and you will see it saying "Hello, world!."

$ bin/jsx --executable web --output hello.jsx.js hello.jsx

Further Learning

More documents can be found on the wiki.If you are looking for examples, please refer to the examples on this web site, the example directory of the distribution, or to the links on Resources page of the wiki.

Đọc thêm

JSX is an embeddable XML-like syntax. It is meant to be transformed into valid JavaScript, though the semantics of that transformation are implementation-specific. JSX rose to popularity with the React framework, but has since seen other implementations as well. TypeScript supports embedding, type checking, and compiling JSX directly to JavaScript.

Basic usage

In order to use JSX you must do two things.

  1. Name your files with a .tsx extension

  2. Enable the jsx option

TypeScript ships with three JSX modes: preserve, react, and react-native. These modes only affect the emit stage - type checking is unaffected. The preserve mode will keep the JSX as part of the output to be further consumed by another transform step (e.g. Babel). Additionally the output will have a .jsx file extension. The react mode will emit React.createElement, does not need to go through a JSX transformation before use, and the output will have a .js file extension. The react-native mode is the equivalent of preserve in that it keeps all JSX, but the output will instead have a .js file extension.

Mode

Input

Output

Output File Extension

preserve

<div />

<div />

.jsx

react

<div />

React.createElement("div")

.js

react-native

<div />

<div />

.js

You can specify this mode using either the --jsx command line flag or the corresponding option in your tsconfig.json file.

*Note: You can specify the JSX factory function to use when targeting react JSX emit with --jsxFactory option (defaults to React.createElement)

The as operator

Recall how to write a type assertion:

var foo = <foo>bar;

This asserts the variable bar to have the type foo. Since TypeScript also uses angle brackets for type assertions, combining it with JSX’s syntax would introduce certain parsing difficulties. As a result, TypeScript disallows angle bracket type assertions in .tsx files.

Since the above syntax cannot be used in .tsx files, an alternate type assertion operator should be used: as. The example can easily be rewritten with the as operator.

var foo = bar as foo;

The as operator is available in both .ts and .tsx files, and is identical in behavior to the angle-bracket type assertion style.

Type Checking

In order to understand type checking with JSX, you must first understand the difference between intrinsic elements and value-based elements. Given a JSX expression <expr />, expr may either refer to something intrinsic to the environment (e.g. a div or span in a DOM environment) or to a custom component that you’ve created. This is important for two reasons:

  1. For React, intrinsic elements are emitted as strings (React.createElement("div")), whereas a component you’ve created is not (React.createElement(MyComponent)).

  2. The types of the attributes being passed in the JSX element should be looked up differently. Intrinsic element attributes should be known intrinsically whereas components will likely want to specify their own set of attributes.

TypeScript uses the same convention that React does for distinguishing between these. An intrinsic element always begins with a lowercase letter, and a value-based element always begins with an uppercase letter.

Intrinsic elements

Intrinsic elements are looked up on the special interface JSX.IntrinsicElements. By default, if this interface is not specified, then anything goes and intrinsic elements will not be type checked. However, if this interface is present, then the name of the intrinsic element is looked up as a property on the JSX.IntrinsicElements interface. For example:

declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
  }
}

<foo />; // ok
<bar />; // error

In the above example, <foo /> will work fine but <bar /> will result in an error since it has not been specified on JSX.IntrinsicElements.

Note: You can also specify a catch-all string indexer on JSX.IntrinsicElements as follows:

declare namespace JSX {
  interface IntrinsicElements {
    [elemName: string]: any;
  }
}

Value-based elements

Value-based elements are simply looked up by identifiers that are in scope.

import MyComponent from "./myComponent";

<MyComponent />; // ok
<SomeOtherComponent />; // error

There are two ways to define a value-based element:

  1. Function Component (FC)

  2. Class Component

Because these two types of value-based elements are indistinguishable from each other in a JSX expression, first TS tries to resolve the expression as a Function Component using overload resolution. If the process succeeds, then TS finishes resolving the expression to its declaration. If the value fails to resolve as a Function Component, TS will then try to resolve it as a class component. If that fails, TS will report an error.

Function Component

As the name suggests, the component is defined as a JavaScript function where its first argument is a props object. TS enforces that its return type must be assignable to JSX.Element.

interface FooProp {
  name: string;
  X: number;
  Y: number;
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
  return <AnotherComponent name={prop.name} />;
}

const Button = (prop: {value: string}, context: { color: string }) => <button>

Because a Function Component is simply a JavaScript function, function overloads may be used here as well:

interface ClickableProps {
  children: JSX.Element[] | JSX.Element
}

interface HomeProps extends ClickableProps {
  home: JSX.Element;
}

interface SideProps extends ClickableProps {
  side: JSX.Element | string;
}

function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
  ...
}

Note: Function Components were formerly known as Stateless Function Components (SFC). As Function Components can no longer be considered stateless in recent versions of react, the type SFC and its alias StatelessComponent were deprecated.

Class Component

It is possible to define the type of a class component. However, to do so it is best to understand two new terms: the element class type and the element instance type.

Given <Expr />, the element class type is the type of Expr. So in the example above, if MyComponent was an ES6 class the class type would be that class’s constructor and statics. If MyComponent was a factory function, the class type would be that function.

Once the class type is established, the instance type is determined by the union of the return types of the class type’s construct or call signatures (whichever is present). So again, in the case of an ES6 class, the instance type would be the type of an instance of that class, and in the case of a factory function, it would be the type of the value returned from the function.

class MyComponent {
  render() {}
}

// use a construct signature
var myComponent = new MyComponent();

// element class type => MyComponent
// element instance type => { render: () => void }

function MyFactoryFunction() {
  return {
    render: () => {}
  };
}

// use a call signature
var myComponent = MyFactoryFunction();

// element class type => FactoryFunction
// element instance type => { render: () => void }

The element instance type is interesting because it must be assignable to JSX.ElementClass or it will result in an error. By default JSX.ElementClass is {}, but it can be augmented to limit the use of JSX to only those types that conform to the proper interface.

declare namespace JSX {
  interface ElementClass {
    render: any;
  }
}

class MyComponent {
  render() {}
}
function MyFactoryFunction() {
  return { render: () => {} };
}

<MyComponent />; // ok
<MyFactoryFunction />; // ok

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<NotAValidComponent />; // error
<NotAValidFactoryFunction />; // error

Attribute type checking

The first step to type checking attributes is to determine the element attributes type. This is slightly different between intrinsic and value-based elements.

For intrinsic elements, it is the type of the property on JSX.IntrinsicElements

declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean };
  }
}

// element attributes type for 'foo' is '{bar?: boolean}'
<foo bar />;

For value-based elements, it is a bit more complex. It is determined by the type of a property on the element instance type that was previously determined. Which property to use is determined by JSX.ElementAttributesProperty. It should be declared with a single property. The name of that property is then used. As of TypeScript 2.8, if JSX.ElementAttributesProperty is not provided, the type of first parameter of the class element’s constructor or Function Component’s call will be used instead.

declare namespace JSX {
  interface ElementAttributesProperty {
    props; // specify the property name to use
  }
}

class MyComponent {
  // specify the property on the element instance type
  props: {
    foo?: string;
  };
}

// element attributes type for 'MyComponent' is '{foo?: string}'
<MyComponent foo="bar" />;

The element attribute type is used to type check the attributes in the JSX. Optional and required properties are supported.

declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number };
  }
}

<foo requiredProp="bar" />; // ok
<foo requiredProp="bar" optionalProp={0} />; // ok
<foo />; // error, requiredProp is missing
<foo requiredProp={0} />; // error, requiredProp should be a string
<foo requiredProp="bar" unknownProp />; // error, unknownProp does not exist
<foo requiredProp="bar" some-unknown-prop />; // ok, because 'some-unknown-prop' is not a valid identifier

Note: If an attribute name is not a valid JS identifier (like a data-* attribute), it is not considered to be an error if it is not found in the element attributes type.

Additionally, the JSX.IntrinsicAttributes interface can be used to specify extra properties used by the JSX framework which are not generally used by the components’ props or arguments - for instance key in React. Specializing further, the generic JSX.IntrinsicClassAttributes<T> type may also be used to specify the same kind of extra attributes just for class components (and not Function Components). In this type, the generic parameter corresponds to the class instance type. In React, this is used to allow the ref attribute of type Ref<T>. Generally speaking, all of the properties on these interfaces should be optional, unless you intend that users of your JSX framework need to provide some attribute on every tag.

The spread operator also works:

var props = { requiredProp: "bar" };
<foo {...props} />; // ok

var badProps = {};
<foo {...badProps} />; // error

Children Type Checking

In TypeScript 2.3, TS introduced type checking of children. children is a special property in an element attributes type where child JSXExpressions are taken to be inserted into the attributes. Similar to how TS uses JSX.ElementAttributesProperty to determine the name of props, TS uses JSX.ElementChildrenAttribute to determine the name of children within those props. JSX.ElementChildrenAttribute should be declared with a single property.

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: {}; // specify children name to use
  }
}
<div>
  <h1>Hello</h1>
</div>;

<div>
  <h1>Hello</h1>
  World
</div>;

const CustomComp = (props) => <div>{props.children}</div>
<CustomComp>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</CustomComp>

You can specify the type of children like any other attribute. This will override the default type from, eg the React typings if you use them.

interface PropsType {
  children: JSX.Element
  name: string
}

class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

// OK
<Component name="foo">
  <h1>Hello World</h1>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element
<Component name="bar">
  <h1>Hello World</h1>
  <h2>Hello World</h2>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element or string.
<Component name="baz">
  <h1>Hello</h1>
  World
</Component>

The JSX result type

By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.

Embedding Expressions

JSX allows you to embed expressions between tags by surrounding the expressions with curly braces ({ }).

var a = <div>
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

The above code will result in an error since you cannot divide a string by a number. The output, when using the preserve option, looks like:

var a = <div>
  {["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React integration

To use JSX with React you should use the React typings. These typings define the JSX namespace appropriately for use with React.

/// <reference path="react.d.ts" />

interface Props {
  foo: string;
}

class MyComponent extends React.Component<Props, {}> {
  render() {
    return <span>{this.props.foo}</span>;
  }
}

<MyComponent foo="bar" />; // ok
<MyComponent foo={0} />; // error

Factory Functions

The exact factory function used by the jsx: react compiler option is configurable. It may be set using either the jsxFactory command line option, or an inline @jsx comment pragma to set it on a per-file basis. For example, if you set jsxFactory to createElement, <div /> will emit as createElement("div") instead of React.createElement("div").

The comment pragma version may be used like so (in TypeScript 2.8):

import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

emits as:

const preact = require("preact");
const x = preact.h("div", null);

The factory chosen will also affect where the JSX namespace is looked up (for type checking information) before falling back to the global one. If the factory is defined as React.createElement (the default), the compiler will check for React.JSX before checking for a global JSX. If the factory is defined as h, it will check for h.JSX before a global JSX.

Last updated

Was this helpful?