JSX

JSX 是一种可嵌入的类 XML 语法。它旨在转换为有效的 JavaScript,尽管该转换的语义是特定于实现的。JSX 随着 React 框架而流行起来,但后来也看到了其他实现。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。

¥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

为了使用 JSX,你必须做两件事。

¥In order to use JSX you must do two things.

  1. 使用 .tsx 扩展名命名你的文件

    ¥Name your files with a .tsx extension

  2. 启用 jsx 选项

    ¥Enable the jsx option

TypeScript 附带几种 JSX 模式:preservereact(经典运行时)、react-jsx(自动运行时)、react-jsxdev(自动开发运行时)和 react-nativepreserve 模式将保持 JSX 作为输出的一部分,以供另一个转换步骤(例如 Babel)进一步使用。此外,输出将具有 .jsx 文件扩展名。react 模式会触发 React.createElement,使用前不需要经过 JSX 转换,输出会有 .js 文件扩展名。react-native 模式等价于 preserve,因为它保留所有 JSX,但输出将改为具有 .js 文件扩展名。

¥TypeScript ships with several JSX modes: preserve, react (classic runtime), react-jsx (automatic runtime), react-jsxdev (automatic development runtime), and react-native. 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.

模式 输入 输出 输出文件扩展名
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js
react-native <div /> <div /> .js
react-jsx <div /> _jsx("div", {}, void 0); .js
react-jsxdev <div /> _jsxDEV("div", {}, void 0, false, {...}, this); .js

你可以使用 jsx 命令行标志或相应的选项 tsconfig.json 中的 jsx 文件指定此模式。

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

*注意:你可以使用 jsxFactory 选项指定 JSX 工厂函数(默认为 React.createElement

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

as 运算符

¥The as operator

回想一下如何编写类型断言:

¥Recall how to write a type assertion:

ts
const foo = <foo>bar;

这将变量 bar 断言为具有 foo 类型。由于 TypeScript 也使用尖括号来进行类型断言,将它与 JSX 的语法结合起来会带来一定的解析困难。因此,TypeScript 不允许在 .tsx 文件中使用尖括号类型断言。

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

由于上述语法不能在 .tsx 文件中使用,因此应使用替代类型断言运算符:as。该示例可以很容易地用 as 运算符重写。

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

ts
const foo = bar as foo;

as 运算符在 .ts.tsx 文件中都可用,并且在行为上与尖括号类型断言样式相同。

¥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

为了理解 JSX 的类型检查,你必须首先了解内在元素和基于值的元素之间的区别。给定一个 JSX 表达式 <expr />expr 可以指代环境固有的东西(例如 DOM 环境中的 divspan),也可以指你创建的自定义组件。这很重要,原因有两个:

¥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. 对于 React,内在元素作为字符串 (React.createElement("div")) 触发,而你创建的组件不是 (React.createElement(MyComponent))。

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

  2. 在 JSX 元素中传递的属性类型应该以不同的方式查找。内在元素属性应该是内在已知的,而组件可能希望指定它们自己的属性集。

    ¥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 使用 与 React 相同的约定 来区分这些。内在元素总是以小写字母开头,而基于值的元素总是以大写字母开头。

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

JSX 命名空间

¥The JSX namespace

TypeScript 中的 JSX 由 JSX 命名空间键入。JSX 命名空间可以在各个地方定义,具体取决于 jsx 编译器选项。

¥JSX in TypeScript is typed by the JSX namespace. The JSX namespace may be defined in various places, depending on the jsx compiler option.

jsx 选项 preservereactreact-native 使用经典运行时的类型定义。这意味着变量需要在由 jsxFactory 编译器选项确定的范围内。JSX 命名空间应在 JSX 工厂的最顶层标识符上指定。例如,React 使用默认工厂 React.createElement。这意味着它的 JSX 命名空间应定义为 React.JSX

¥The jsx options preserve, react, and react-native use the type definitions for classic runtime. This means a variable needs to be in scope that’s determined by the jsxFactory compiler option. The JSX namespace should be specified on the top-most identifier of the JSX factory. For example, React uses the default factory React.createElement. This means its JSX namespace should be defined as React.JSX.

ts
export function createElement(): any;
export namespace JSX {
// …
}

并且用户应始终将 React 导入为 React

¥And the user should always import React as React.

ts
import * as React from 'react';

Preact 使用 JSX 工厂 h。这意味着它的类型应该定义为 h.JSX

¥Preact uses the JSX factory h. That means its types should be defined as the h.JSX.

ts
export function h(props: any): any;
export namespace h.JSX {
// …
}

用户应使用命名导入来导入 h

¥The user should use a named import to import h.

ts
import { h } from 'preact';

对于 jsx 选项 react-jsxreact-jsxdev,应从匹配的入口点导出 JSX 命名空间。对于 react-jsx,这是 ${jsxImportSource}/jsx-runtime。对于 react-jsxdev,这是 ${jsxImportSource}/jsx-dev-runtime。由于这些不使用文件扩展名,因此必须在 package.json 映射中使用 exports 字段才能支持 ESM 用户。

¥For the jsx options react-jsx and react-jsxdev, the JSX namespace should be exported from the matching entry points. For react-jsx this is ${jsxImportSource}/jsx-runtime. For react-jsxdev, this is ${jsxImportSource}/jsx-dev-runtime. Since these don’t use a file extension, you must use the exports field in package.json map in order to support ESM users.

json
{
"exports": {
"./jsx-runtime": "./jsx-runtime.js",
"./jsx-dev-runtime": "./jsx-dev-runtime.js",
}
}

然后在 jsx-runtime.d.tsjsx-dev-runtime.d.ts 中:

¥Then in jsx-runtime.d.ts and jsx-dev-runtime.d.ts:

ts
export namespace JSX {
// …
}

请注意,虽然导出 JSX 命名空间足以进行类型检查,但生产运行时在运行时需要 jsxjsxsFragment 导出,而开发运行时需要 jsxDEVFragment。理想情况下,你也为这些添加类型。

¥Note that while exporting the JSX namespace is sufficient for type checking, the production runtime needs the jsx, jsxs, and Fragment exports at runtime, and the development runtime needs jsxDEV and Fragment. Ideally you add types for those too.

如果 JSX 命名空间在适当位置不可用,则经典和自动运行时都会回退到全局 JSX 命名空间。

¥If the JSX namespace isn’t available in the appropriate location, both the classic and the automatic runtime fall back to the global JSX namespace.

内在要素

¥Intrinsic elements

在特殊接口 JSX.IntrinsicElements 上查找内在元素。默认情况下,如果未指定此接口,则任何事情都会发生,并且不会对内部元素进行类型检查。但是,如果此接口存在,则内部元素的名称将作为 JSX.IntrinsicElements 接口上的属性进行查找。例如:

¥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:

tsx
declare namespace JSX {
interface IntrinsicElements {
foo: any;
}
}
<foo />; // ok
<bar />; // error

在上面的示例中,<foo /> 可以正常工作,但 <bar /> 将导致错误,因为它没有在 JSX.IntrinsicElements 上指定。

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

注意:你还可以在 JSX.IntrinsicElements 上指定一个包罗万象的字符串索引器,如下所示:

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

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

基于值的元素

¥Value-based elements

基于值的元素只需通过作用域内的标识符进行查找。

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

tsx
import MyComponent from "./myComponent";
<MyComponent />; // ok
<SomeOtherComponent />; // error

有两种方法可以定义基于值的元素:

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

  1. 函数组件 (FC)

    ¥Function Component (FC)

  2. 类组件

    ¥Class Component

因为这两种类型的基于值的元素在 JSX 表达式中彼此无法区分,所以首先 TS 尝试使用重载解析将表达式解析为函数组件。如果该过程成功,则 TS 完成将表达式解析为其声明。如果该值无法解析为函数组件,则 TS 将尝试将其解析为类组件。如果失败,TS 将报告错误。

¥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

顾名思义,该组件被定义为一个 JavaScript 函数,其第一个参数是一个 props 对象。TS 强制其返回类型必须可分配给 JSX.Element

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

tsx
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 />
);

因为函数组件只是一个 JavaScript 函数,所以这里也可以使用函数重载:

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

ts
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;
function MainButton(prop: ClickableProps): JSX.Element {
// ...
}
Try

注意:函数组件以前称为无状态函数组件 (SFC)。由于函数组件在最近版本的 react 中不再被认为是无状态的,所以类型 SFC 及其别名 StatelessComponent 已被弃用。

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

给定 <Expr />,元素类类型是 Expr 的类型。所以在上面的例子中,如果 MyComponent 是一个 ES6 类,那么类类型就是该类的构造函数和静态变量。如果 MyComponent 是一个工厂函数,那么类类型就是那个函数。

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

一旦建立了类类型,实例类型由类类型构造的返回类型或调用签名(以存在者为准)的联合确定。同样,在 ES6 类的情况下,实例类型将是该类的实例的类型,而在工厂函数的情况下,它将是从函数返回的值的类型。

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

ts
class MyComponent {
render() {}
}
// use a construct signature
const myComponent = new MyComponent();
// element class type => MyComponent
// element instance type => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {},
};
}
// use a call signature
const myComponent = MyFactoryFunction();
// element class type => MyFactoryFunction
// element instance type => { render: () => void }

元素实例类型很有趣,因为它必须可以分配给 JSX.ElementClass,否则会导致错误。默认情况下 JSX.ElementClass{},但可以扩展它以将 JSX 的使用限制为仅符合正确接口的那些类型。

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

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

对于内在元素,它是 JSX.IntrinsicElements 上的属性类型

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

tsx
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean };
}
}
// element attributes type for 'foo' is '{bar?: boolean}'
<foo bar />;

对于基于值的元素,它有点复杂。它由先前确定的元素实例类型上的属性类型确定。使用哪个属性由 JSX.ElementAttributesProperty 决定。它应该用一个属性声明。然后使用该属性的名称。从 TypeScript 2.8 开始,如果未提供 JSX.ElementAttributesProperty,则将使用类元素的构造函数或函数组件调用的第一个参数的类型。

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

tsx
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" />;

element 属性类型用于对 JSX 中的属性进行类型检查。支持可选和必需的属性。

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

tsx
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

注意:如果属性名称不是有效的 JS 标识符(如 data-* 属性),如果在元素属性类型中找不到它,则不认为是错误。

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

此外,JSX.IntrinsicAttributes 接口可用于指定 JSX 框架使用的额外属性,这些属性通常不用于组件的 props 或参数 - 例如 React 中的 key。进一步专门化,泛型 JSX.IntrinsicClassAttributes<T> 类型也可用于为类组件(而不是函数组件)指定相同类型的额外属性。在这种类型中,泛型参数对应于类实例类型。在 React 中,这用于允许 Ref<T> 类型的 ref 属性。一般来说,这些接口上的所有属性都应该是可选的,除非你打算让 JSX 框架的用户需要在每个标签上提供一些属性。

¥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:

tsx
const props = { requiredProp: "bar" };
<foo {...props} />; // ok
const badProps = {};
<foo {...badProps} />; // error

子类型检查

¥Children Type Checking

在 TypeScript 2.3 中,TS 引入了子项的类型检查。children 是元素属性类型中的一个特殊属性,其中子 JSXExpressions 被插入到属性中。类似于 TS 使用 JSX.ElementAttributesProperty 来确定 props 的名称,TS 使用 JSX.ElementChildrenAttribute 来确定这些 props 中的子级的名字。JSX.ElementChildrenAttribute 应使用单个属性声明。

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

ts
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // specify children name to use
}
}
tsx
<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>

你可以像任何其他属性一样指定子项的类型。这将覆盖默认类型,例如 React 类型,如果你使用它们。

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

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

JSX 结果类型

¥The JSX result type

默认情况下,JSX 表达式的结果类型为 any。你可以通过指定 JSX.Element 接口来自定义类型。但是,无法从此接口检索有关 JSX 的元素、属性或子项的类型信息。这是一个黑匣子。

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

JSX 函数返回类型

¥The JSX function return type

默认情况下,函数组件必须返回 JSX.Element | null。但是,这并不总是代表运行时行为。从 TypeScript 5.1 开始,你可以指定 JSX.ElementType 来覆盖有效的 JSX 组件类型。请注意,这并未定义哪些 props 是有效的。props 的类型始终由传递的组件的第一个参数定义。默认值如下所示:

¥By default, function components must return JSX.Element | null. However, this doesn’t always represent runtime behaviour. As of TypeScript 5.1, you can specify JSX.ElementType to override what is a valid JSX component type. Note that this doesn’t define what props are valid. The type of props is always defined by the first argument of the component that’s passed. The default looks something like this:

ts
namespace JSX {
export type ElementType =
// All the valid lowercase tags
keyof IntrinsicAttributes
// Function components
(props: any) => Element
// Class components
new (props: any) => ElementClass;
export interface IntrinsicAttributes extends /*...*/ {}
export type Element = /*...*/;
export type ElementClass = /*...*/;
}

嵌入表达式

¥Embedding Expressions

JSX 允许你通过用大括号 ({ }) 包围表达式来在标签之间嵌入表达式。

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

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

上面的代码将导致错误,因为你不能将字符串除以数字。使用 preserve 选项时的输出如下所示:

¥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:

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

React 集成

¥React integration

要将 JSX 与 React 一起使用,你应该使用 React 类型。这些类型定义了 JSX 命名空间,以便与 React 一起使用。

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

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

配置 JSX

¥Configuring JSX

有多个编译器标志可用于自定义 JSX,它们既可用作编译器标志,也可通过内联的每个文件编译指示工作。要了解更多信息,请参阅他们的 tsconfig 参考页面:

¥There are multiple compiler flags which can be used to customize your JSX, which work as both a compiler flag and via inline per-file pragmas. To learn more see their tsconfig reference pages: