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.
TypeScript 附带几种 JSX 模式:preserve
、react
(经典运行时)、react-jsx
(自动运行时)、react-jsxdev
(自动开发运行时)和 react-native
。preserve
模式将保持 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 toReact.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 环境中的 div
或 span
),也可以指你创建的自定义组件。这很重要,原因有两个:
¥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:
-
对于 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)
). -
在 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
选项 preserve
、react
和 react-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-jsx
和 react-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.ts
和 jsx-dev-runtime.d.ts
中:
¥Then in jsx-runtime.d.ts
and jsx-dev-runtime.d.ts
:
ts
export namespace JSX {// …}
请注意,虽然导出 JSX
命名空间足以进行类型检查,但生产运行时在运行时需要 jsx
、jsxs
和 Fragment
导出,而开发运行时需要 jsxDEV
和 Fragment
。理想情况下,你也为这些添加类型。
¥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:
-
函数组件 (FC)
¥Function Component (FC)
-
类组件
¥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:
tsTry
interfaceClickableProps {children :JSX .Element [] |JSX .Element ;}interfaceHomeProps extendsClickableProps {home :JSX .Element ;}interfaceSideProps extendsClickableProps {side :JSX .Element | string;}functionMainButton (prop :HomeProps ):JSX .Element ;functionMainButton (prop :SideProps ):JSX .Element ;functionMainButton (prop :ClickableProps ):JSX .Element {// ...}
注意:函数组件以前称为无状态函数组件 (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 aliasStatelessComponent
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 signatureconst myComponent = new MyComponent();// element class type => MyComponent// element instance type => { render: () => void }function MyFactoryFunction() {return {render: () => {},};}// use a call signatureconst 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 />; // okclass 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 typeprops: {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} />; // okconst 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.Elementname: 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 IntrinsicElements// 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: