JSX
JSX 是一种可嵌入的类似 XML 的语法。 它旨在被转换为有效的 JavaScript,尽管这种转换的语义是特定于实现的。 JSX 随着 React 框架而流行起来,但此后也出现了其他实现。 TypeScript 支持嵌入、类型检查以及将 JSX 直接编译为 JavaScript。
基本用法
要使用 JSX,你必须做两件事。
- 使用
.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 文件扩展名。
| 模式 | 输入 | 输出 | 输出文件扩展名 |
|---|---|---|---|
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 选项来指定此模式。
*注意:你可以使用
jsxFactory选项(默认为React.createElement)指定针对 react JSX 输出时要使用的 JSX 工厂函数。
as 操作符
回忆一下如何编写类型断言:
const foo = <Foo>bar;这断言变量 bar 具有类型 Foo。 由于 TypeScript 也使用尖括号进行类型断言,将其与 JSX 的语法结合会引入一些解析困难。因此,TypeScript 不允许在 .tsx 文件中使用尖括号类型断言。
由于上述语法不能在 .tsx 文件中使用,应该使用替代的类型断言操作符:as。 该示例可以轻松地使用 as 操作符重写。
const foo = bar as Foo;as 操作符在 .ts 和 .tsx 文件中都可用,其行为与尖括号类型断言风格相同。
类型检查
为了理解 JSX 的类型检查,你必须首先理解内置元素和基于值的元素之间的区别。 给定一个 JSX 表达式 <expr />,expr 可能引用环境内置的内容(例如 DOM 环境中的 div 或 span),也可能引用你创建的自定义组件。 这很重要,原因有二:
- 对于 React,内置元素作为字符串输出(
React.createElement("div")),而你创建的组件则不然(React.createElement(MyComponent))。 - 在 JSX 元素中传递的属性的类型应该以不同的方式查找。 内置元素的属性应该是内在已知的,而组件则可能希望指定它们自己的一组属性。
TypeScript 使用 React 使用的相同约定来区分它们。 内置元素总是以小写字母开头,而基于值的元素总是以大写字母开头。
JSX 命名空间
TypeScript 中的 JSX 通过 JSX 命名空间进行类型化。JSX 命名空间可以根据 jsx 编译器选项在不同的地方定义。
jsx 选项 preserve、react 和 react-native 使用经典运行时的类型定义。这意味着需要在作用域内有一个由 jsxFactory 编译器选项确定的变量。JSX 命名空间应该指定在 JSX 工厂的最顶层标识符上。例如,React 使用默认工厂 React.createElement。这意味着它的 JSX 命名空间应该定义为 React.JSX。
export function createElement(): any;
export namespace JSX {
// …
}并且用户应该始终将 React 导入为 React。
import * as React from 'react';Preact 使用 JSX 工厂 h。这意味着它的类型应该定义为 h.JSX。
export function h(props: any): any;
export namespace h.JSX {
// …
}用户应该使用命名导入来导入 h。
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 用户。
{
"exports": {
"./jsx-runtime": "./jsx-runtime.js",
"./jsx-dev-runtime": "./jsx-dev-runtime.js",
}
}然后在 jsx-runtime.d.ts 和 jsx-dev-runtime.d.ts 中:
export namespace JSX {
// …
}注意,虽然导出 JSX 命名空间足以进行类型检查,但生产运行时需要在运行时导出 jsx、jsxs 和 Fragment,而开发运行时需要 jsxDEV 和 Fragment。理想情况下,你也为这些添加类型。
如果 JSX 命名空间在适当的位置不可用,经典运行时和自动运行时都会回退到全局 JSX 命名空间。
内置元素
内置元素在特殊接口 JSX.IntrinsicElements 上查找。 默认情况下,如果未指定此接口,则一切正常,并且不会对内置元素进行类型检查。 但是,如果存在此接口,则内置元素的名称将作为 JSX.IntrinsicElements 接口上的属性进行查找。 例如:
declare namespace JSX {
interface IntrinsicElements {
foo: any;
}
}
<foo />; // ok
<bar />; // error在上面的示例中,<foo /> 将正常工作,但 <bar /> 将导致错误,因为它未在 JSX.IntrinsicElements 上指定。
注意:你也可以在
JSX.IntrinsicElements上指定一个包罗万象的字符串索引器,如下所示:
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}基于值的元素
基于值的元素只需通过作用域内的标识符进行查找。
import MyComponent from "./myComponent";
<MyComponent />; // ok
<SomeOtherComponent />; // error定义基于值的元素有两种方式:
- 函数组件(FC)
- 类组件
由于这两种类型的基于值的元素在 JSX 表达式中彼此无法区分,因此 TS 首先尝试使用重载解析将表达式解析为函数组件。如果该过程成功,则 TS 完成将其解析为其声明。如果该值无法解析为函数组件,TS 将尝试将其解析为类组件。如果失败,TS 将报告错误。
函数组件
顾名思义,组件被定义为 JavaScript 函数,其第一个参数是 props 对象。 TS 强制其返回类型必须可赋值给 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 />
);因为函数组件只是一个 JavaScript 函数,这里也可以使用函数重载:
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已被弃用。
类组件
定义类组件的类型是可能的。 但是,要做到这一点,最好理解两个新术语:元素类类型和元素实例类型。
给定 <Expr />,元素类类型是 Expr 的类型。 因此,在上面的示例中,如果 MyComponent 是一个 ES6 类,则类类型将是该类的构造函数和静态部分。 如果 MyComponent 是一个工厂函数,则类类型将是该函数。
一旦确定了类类型,实例类型就由类类型的构造函数或调用签名(以存在的为准)的返回类型的联合确定。 因此,再次以 ES6 类为例,实例类型将是该类实例的类型,而对于工厂函数,它将是该函数返回的值的类型。
class MyComponent {
render() {}
}
// 使用构造签名
const myComponent = new MyComponent();
// 元素类类型 => MyComponent
// 元素实例类型 => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {},
};
}
// 使用调用签名
const myComponent = MyFactoryFunction();
// 元素类类型 => MyFactoryFunction
// 元素实例类型 => { render: () => void }元素实例类型很有趣,因为它必须可赋值给 JSX.ElementClass,否则将导致错误。 默认情况下,JSX.ElementClass 是 {},但可以对其进行扩展,以将 JSX 的使用限制为仅符合适当接口的类型。
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属性类型检查
类型检查属性的第一步是确定元素属性类型。 这在内置元素和基于值的元素之间略有不同。
对于内置元素,它是 JSX.IntrinsicElements 上属性的类型
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean };
}
}
// 'foo' 的元素属性类型是 '{bar?: boolean}'
<foo bar />;对于基于值的元素,它稍微复杂一些。 它由先前确定的元素实例类型上的属性类型决定。 使用哪个属性由 JSX.ElementAttributesProperty 决定。 它应该声明一个单一属性。 然后使用该属性的名称。 从 TypeScript 2.8 开始,如果没有提供 JSX.ElementAttributesProperty,则将使用类元素的构造函数或函数组件调用的第一个参数的类型代替。
declare namespace JSX {
interface ElementAttributesProperty {
props; // 指定要使用的属性名称
}
}
class MyComponent {
// 指定元素实例类型上的属性
props: {
foo?: string;
};
}
// 'MyComponent' 的元素属性类型是 '{foo?: string}'
<MyComponent foo="bar" />;元素属性类型用于类型检查 JSX 中的属性。 支持可选和必需属性。
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number };
}
}
<foo requiredProp="bar" />; // ok
<foo requiredProp="bar" optionalProp={0} />; // ok
<foo />; // error, requiredProp 缺失
<foo requiredProp={0} />; // error, requiredProp 应该是 string
<foo requiredProp="bar" unknownProp />; // error, unknownProp 不存在
<foo requiredProp="bar" some-unknown-prop />; // ok, 因为 'some-unknown-prop' 不是有效的标识符注意:如果属性名称不是有效的 JS 标识符(如
data-*属性),则在元素属性类型中找不到它不会被视为错误。
此外,JSX.IntrinsicAttributes 接口可用于指定 JSX 框架使用的额外属性,这些属性通常不由组件的 props 或参数使用——例如 React 中的 key。进一步特化,泛型 JSX.IntrinsicClassAttributes<T> 类型也可用于仅为类组件(而不是函数组件)指定相同种类的额外属性。在此类型中,泛型参数对应于类实例类型。在 React 中,这用于允许 ref 属性,其类型为 Ref<T>。一般来说,这些接口上的所有属性都应该是可选的,除非你打算让你的 JSX 框架的用户需要在每个标签上提供某些属性。
展开运算符也适用:
const props = { requiredProp: "bar" };
<foo {...props} />; // ok
const badProps = {};
<foo {...badProps} />; // error子元素类型检查
在 TypeScript 2.3 中,TS 引入了对子元素的类型检查。子元素是元素属性类型中的一个特殊属性,其中子 JSXExpression 被视为插入到属性中。 类似于 TS 使用 JSX.ElementAttributesProperty 来确定 props 的名称,TS 使用 JSX.ElementChildrenAttribute 来确定这些 props 中 children 的名称。 JSX.ElementChildrenAttribute 应该声明一个单一属性。
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // 指定 children 的名称
}
}<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 类型定义中的默认类型。
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 的类型是 JSX.Element 而不是 JSX.Element 数组
<Component name="bar">
<h1>Hello World</h1>
<h2>Hello World</h2>
</Component>
// Error: children 的类型是 JSX.Element 而不是 JSX.Element 数组或字符串
<Component name="baz">
<h1>Hello</h1>
World
</Component>JSX 结果类型
默认情况下,JSX 表达式的结果类型为 any。 你可以通过指定 JSX.Element 接口来自定义类型。 但是,无法从此接口检索有关 JSX 的元素、属性或子元素的类型信息。 它是一个黑盒。
JSX 函数返回类型
默认情况下,函数组件必须返回 JSX.Element | null。然而,这并不总是代表运行时行为。从 TypeScript 5.1 开始,你可以指定 JSX.ElementType 来覆盖什么是有效的 JSX 组件类型。注意,这并不定义哪些 props 是有效的。props 的类型总是由传入的组件的第一个参数定义。默认值如下所示:
namespace JSX {
export type ElementType =
// 所有有效的小写标签
| keyof IntrinsicElements
// 函数组件
| (props: any) => Element
// 类组件
| new (props: any) => ElementClass;
export interface IntrinsicAttributes extends /*...*/ {}
export type Element = /*...*/;
export type ElementClass = /*...*/;
}嵌入表达式
JSX 允许你在标签之间通过用花括号({ })将表达式括起来来嵌入表达式。
const a = (
<div>
{["foo", "bar"].map((i) => (
<span>{i / 2}</span>
))}
</div>
);上面的代码将导致错误,因为你不能将字符串除以数字。 使用 preserve 选项时,输出如下:
const a = (
<div>
{["foo", "bar"].map(function (i) {
return <span>{i / 2}</span>;
})}
</div>
);React 集成
要将 JSX 与 React 一起使用,你应该使用 React 类型定义。 这些类型定义为与 React 一起使用而恰当地定义了 JSX 命名空间。
/// <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
有多个编译器标志可用于自定义你的 JSX,它们既可以作为编译器标志,也可以通过内联的每个文件 pragma 来使用。要了解更多信息,请参阅它们的 tsconfig 参考页面: