TypeScript Office - JSX
# 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 模式相当于保留模式,它保留了所有的 JSX,但输出将有一个 .js
文件扩展名。
Mode | Input | Output | Output 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 指定这种模式。
注意:你可以as操作符用 jsxFactory 选项指定针对 react JSX 生成 JS 时使用的 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 既可以指环境中固有的东西(例如 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 />; // 正确
<bar />; // 错误
2
3
4
5
6
7
在上面的例子中,<foo />
可以正常工作,但会导致一个错误,因为它没有被指定在 JSX.IntrinsicElements
上。
注意:你也可以在 JWX.IntrinsicElements
上指定一个全面的字符串索引器,如下所示:
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
2
3
4
5
# 基于值的元素
基于值的元素只是通过范围内的标识符进行查询。
import MyComponent from "./myComponent";
<MyComponent />; // 正确
<SomeOtherComponent />; // 错误
2
3
有两种方法来定义基于值的元素:
- 函数组件(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 />
);
2
3
4
5
6
7
8
9
10
11
12
因为函数组件只是一个 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 {
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意:函数组件以前被称为无状态函数组件(SFC)。由于 Function Components 在最近的 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 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
元素实例类型很有趣,因为它必须可以分配给 JSX.ElementClass
,否则会导致错误。默认情况下,JSX.ElementClass
是 {},但它可以被增强,以限制 JSX 的使用,使其只适用于那些符合适当接口的类型。
declare namespace JSX {
interface ElementClass {
render: any;
}
}
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} };
}
<MyComponent />; // 正确
<MyFactoryFunction />; // 正确
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<NotAValidComponent />; // 错误
<NotAValidFactoryFunction />; // 错误
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 属性类型检查
类型检查属性的第一步是确定元素属性类型。这在内在元素和基于值的元素之间略有不同。
对于内在元素,它是 JSX.IntrinsicElements
上的属性类型。
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean };
}
}
// 'foo'的元素属性类型是'{bar?: boolean}'
<foo bar />;
2
3
4
5
6
7
元素属性类型是用来对 JSX 中的属性进行类型检查的。支持可选和必需的属性。
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number };
}
}
2
3
4
5
<foo requiredProp="bar" />; // 正确
<foo requiredProp="bar" optionalProp={0} />; // 正确
<foo />; // 错误, requiredProp 缺失
<foo requiredProp={0} />; // 错误, requiredProp 应该为 string 类型
<foo requiredProp="bar" unknownProp />; // 错误, unknownProp 属性不存在
<foo requiredProp="bar" some-unknown-prop />; // 正确, 因为 'some-unknown-prop' 不是一个有效的属性标识
2
3
4
5
6
注意:如果一个属性名称不是一个有效的 JS 标识符(如
data-*
属性),如果在元素属性类型中找不到它,则不被认为是一个错误。
此外,JSX.IntrinsicAttributes
接口可以用来指定 JSX 框架使用的额外属性,这些属性一般不会被组件的道具或参数使用,例如 React 中的 key。进一步专门化,通用的 JSX.IntrinsicClassAttributes
类型也可以用来为类组件(而不是函数组件)指定同种额外属性。在这种类型中,通用参数与类的实例类型相对应。在 React 中,这被用来允许 Ref 类型的 ref 属性。一般来说,这些接口上的所有属性都应该是可选的,除非你打算让你的 JSX 框架的用户需要在每个标签上提供一些属性。
展开运算符也能正常工作:
const props = { requiredProp: "bar" };
<foo {...props} />; // 正确
const badProps = {};
<foo {...badProps} />; // 错误
2
3
4
# 子类型检查
在 TypeScript 2.3 中,TS 引入了 children 的类型检查。children 是元素属性类型中的一个特殊属性,子的 JSXExpressions 被采取插入属性中。类似于 TS 使用 JSX.ElementAttributesProperty
来确定 props 的 名称,TS 使用 JSX.ElementChildrenAttribute
来确定这些 props 中的 children 的名称。JSX.ElementChildrenAttribute
应该用一个单一的属性来声明。
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // 指定要使用的 children 名称
}
}
2
3
4
5
<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>
2
3
4
5
6
7
8
9
10
11
12
你可以像其他属性一样指定 children 的类型。这将覆盖默认的类型,例如,如果你使用 React 类型的话:
interface PropsType {
children: JSX.Element
name: string
}
class Component extends React.Component<PropsType, {}> {
render() {
return (
<h2>
{this.props.children}
</h2>
)
}
}
// 正确
<Component name="foo">
<h1>Hello World</h1>
</Component>
// 错误: children是JSX.Element的类型,而不是JSX.Element的数组
<Component name="bar">
<h1>Hello World</h1>
<h2>Hello World</h2>
</Component>
// 错误: children是JSX.Element的类型,而不是JSX.Element的数组或字符串。
<Component name="baz">
<h1>Hello</h1>
World
</Component>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# JSX 的结果类型
默认情况下,JSX 表达式的结果被打造成 any 类型。你可以通过指定 JSX.Element
接口来定制类型。然而,不可能从这个接口中检索到关于 JSX 的元素、属性或孩子的类型信息。它是一个黑盒子。
# 嵌入表达式
JSX 允许你通过用大括号 { }
包围表达式,在标签之间嵌入表达式。
const a = (
<div>
{["foo", "bar"].map((i) => (
<span>{i / 2}</span>
))}
</div>
);
2
3
4
5
6
7
上面的代码将导致一个错误,因为你不能用一个字符串除以一个数字。当使用 preserve 选项时,输出结果看起来像:
const a = (
<div>
{["foo", "bar"].map(function (i) {
return <span>{i / 2}</span>;
})}
</div>
);
2
3
4
5
6
7
# React 集成
要在 React 中使用 JSX,你应该使用 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" />; // 正确
<MyComponent foo={0} />; // 错误
2
3
4
5
6
7
8
9
10
11
# 配置 JSX
有多个编译器标志可以用来定制你的 JSX,它们既可以作为编译器标志,也可以通过内联的每个文件实用程序发挥作用。