# JSX
JSX
是一种可嵌入的类 XML 语法。它旨在转换为有效的 JavaScript,尽管该转换的语义是特定于实现的。JSX 随着 React
框架而流行起来,但后来也看到了其他实现。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。
# 基本用法
为了使用 JSX,你必须做两件事。
- 使用
.tsx
扩展名命名您的文件 - 启用
jsx
选项
TypeScript 附带三种 JSX 模式:preserve
、react
和 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
选项指定 JSX 工厂函数(默认为React.createElement
)
# 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.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 函数,所以这里也可以使用函数重载:
declare module JSX {
interface Element {}
interface IntrinsicElements {
[s: string]: any;
}
}
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 {
// ...
}
注意:函数组件以前称为无状态函数组件 (SFC)。由于函数组件在最近版本的 react 中不再被认为是无状态的,所以类型
SFC
及其别名StatelessComponent
已被弃用。
# 类组件
可以定义类组件的类型。然而,这样做最好理解两个新术语:元素类类型和元素实例类型。
给定 <Expr />
,元素类类型是 Expr
的类型。所以在上面的例子中,如果 MyComponent
是一个 ES6 类,那么类类型就是该类的构造函数和静态变量。如果 MyComponent
是一个工厂函数,那么类类型就是那个函数。
一旦建立了类类型,实例类型由类类型构造的返回类型或调用签名(以存在者为准)的联合确定。同样,在 ES6 类的情况下,实例类型将是该类的实例的类型,而在工厂函数的情况下,它将是从函数返回的值的类型。
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 的使用限制为仅符合正确接口的那些类型。
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 };
}
}
// element attributes type for 'foo' is '{bar?: boolean}'
<foo bar />;
对于基于值的元素,它有点复杂。它由先前确定的元素实例类型上的属性类型确定。使用哪个属性由 JSX.ElementAttributesProperty
决定。它应该用一个属性声明。然后使用该属性的名称。从 TypeScript 2.8 开始,如果未提供 JSX.ElementAttributesProperty
,则将使用类元素的构造函数或函数组件调用的第一个参数的类型。
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 中的属性进行类型检查。支持可选和必需的属性。
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-*
属性),如果在元素属性类型中找不到它,则不认为是错误。
此外,JSX.IntrinsicAttributes
接口可用于指定 JSX 框架使用的额外属性,这些属性通常不被组件的道具或参数使用 - 例如 React 中的 key
。进一步专门化,通用 JSX.IntrinsicClassAttributes<T>
类型也可用于为类组件(而不是函数组件)指定相同类型的额外属性。在这种类型中,泛型参数对应于类实例类型。在 React 中,这用于允许 Ref<T>
类型的 ref
属性。一般来说,这些接口上的所有属性都应该是可选的,除非您打算让 JSX 框架的用户需要在每个标签上提供一些属性。
扩展运算符也适用:
const props = { requiredProp: "bar" };
<foo {...props} />; // ok
const badProps = {};
<foo {...badProps} />; // error
# 子类型检查
在 TypeScript 2.3 中,TS 引入了子项的类型检查。children 是元素属性类型中的一个特殊属性,其中子 JSXExpressions 被插入到属性中。类似于 TS 使用 JSX.ElementAttributesProperty
来确定 props 的名称,TS 使用 JSX.ElementChildrenAttribute
来确定这些 props 中的孩子的名字。JSX.ElementChildrenAttribute
应使用单个属性声明。
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>
您可以像任何其他属性一样指定子项的类型。这将覆盖默认类型,例如 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 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 结果类型
默认情况下,JSX 表达式的结果类型为 any
。您可以通过指定 JSX.Element
接口来自定义类型。但是,无法从此接口检索有关 JSX 的元素、属性或子项的类型信息。这是一个黑匣子。
# 嵌入表达式
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 类型
。这些类型定义了 JSX
命名空间,以便与 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
# 配置 JSX
有多个编译器标志可用于自定义 JSX,它们既可用作编译器标志,也可通过内联的每个文件编译指示工作。要了解更多信息,请参阅他们的 tsconfig 参考页面:
jsxFactory
jsxFragmentFactory
jsxImportSource