JSX 是一种可嵌入的类似 XML 的语法。 它旨在被转换为有效的 JavaScript,尽管这种转换的语义取决于具体的实现。 JSX 随着 React 框架的流行而走红,但后来也出现了其他实现。 TypeScript 支持直接嵌入、类型检查和将 JSX 编译为 JavaScript。
基本用法
🌐 Basic usage
为了使用 JSX,你必须做两件事。
🌐 In order to use JSX you must do two things.
- 将文件命名为
.tsx扩展名 - 启用
jsx选项
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选项针对 React JSX 输出时,可以指定要使用的 JSX 工厂函数(默认为React.createElement)
as 运算符
🌐 The as operator
回想一下如何编写类型断言:
🌐 Recall how to write a type assertion:
tsconst 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.
tsconst 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))。 - 在 JSX 元素中传递的属性类型应以不同的方式查找。内置元素的属性应该是 固有的,而组件则可能希望指定它们自己的属性集。
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.
tsexport function createElement(): any;export namespace JSX {// …}
用户应该始终将 React 导入为 React。
🌐 And the user should always import React as React.
tsimport * 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.
tsexport function h(props: any): any;export namespace h.JSX {// …}
用户应使用命名导入来导入 h。
🌐 The user should use a named import to import h.
tsimport { 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:
tsexport 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:
tsxdeclare 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上指定一个通配字符串索引器:
tsdeclare namespace JSX {interface IntrinsicElements {[elemName: string]: any;}}
基于值的元素
🌐 Value-based elements
基于值的元素只需通过作用域内的标识符进行查找。
🌐 Value-based elements are simply looked up by identifiers that are in scope.
tsximport MyComponent from "./myComponent";<MyComponent />; // ok<SomeOtherComponent />; // error
有两种方法可以定义基于值的元素:
🌐 There are two ways to define a value-based element:
- 函数组件 (FC)
- 类组件
因为这两种基于值的元素在 JSX 表达式中无法区分,TypeScript 会首先尝试通过重载解析将表达式解析为函数组件。如果此过程成功,那么 TypeScript 会将表达式解析到它的声明。如果该值无法解析为函数组件,TypeScript 会尝试将其解析为类组件。如果仍然失败,TypeScript 会报告一个错误。
🌐 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.
tsxinterface 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:
tsTryinterfaceClickableProps {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已被弃用。
类组件
🌐 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.
tsclass 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.
tsxdeclare 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
tsxdeclare namespace JSX {interface IntrinsicElements {foo: { bar?: boolean };}}// element attributes type for 'foo' is '{bar?: boolean}'<foo bar />;
对于基于值的元素,这会稍微复杂一些。
它由之前确定的 element 实例类型 的属性类型决定。
使用哪个属性由 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.
tsxdeclare 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" />;
元素属性类型用于对 JSX 中的属性进行类型检查。支持可选和必需属性。
🌐 The element attribute type is used to type check the attributes in the JSX. Optional and required properties are supported.
tsxdeclare 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-*属性),即使在元素属性类型中未找到,它也不被视为错误。
此外,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:
tsxconst props = { requiredProp: "bar" };<foo {...props} />; // okconst badProps = {};<foo {...badProps} />; // error
子类型检查
🌐 Children Type Checking
在 TypeScript 2.3 中,TS 引入了对 children 的类型检查。children 是 元素属性类型 中的一个特殊属性,其中子 JSXExpression 会被认为插入到属性中。
类似于 TS 使用 JSX.ElementAttributesProperty 来确定 props 的名称,TS 使用 JSX.ElementChildrenAttribute 来确定这些 props 中 children 的名称。
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.
tsdeclare 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>
你可以像指定其他属性一样指定 children 的类型。如果你使用例如 React typings,这将覆盖默认类型。
🌐 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.
tsxinterface 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:
tsnamespace 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 ({ }).
tsxconst 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:
tsxconst a = (<div>{["foo", "bar"].map(function (i) {return <span>{i / 2}</span>;})}</div>);
React 集成
🌐 React integration
要在 React 中使用 JSX,你应该使用 React 类型定义。
这些类型定义会为与 React 一起使用的 JSX 命名空间进行适当的定义。
🌐 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: