JSX

状态: 初稿

译注: 为便于区分, 在本章, 原文 property 译做对象属性, attribute 译做标签属性.

介绍

JSX 是一种类 XML 可嵌入语法.
最终它会被转换成有效的 JavaScript 代码, 转换的语义取决于具体实现.
JSX 随 React 被广为接受, 从此也出现了很多其他实现.
TypeScript 支持嵌入, 类型检查, 直接向 JavaScript 编译 JSX.

基本用法

在使用 JSX 之前, 你必须完成以下两件事:

  1. .tsx 扩展名命名你的文件
  2. 启用 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

你可以用 --jsx 命令行标志或你 tsconfig.json 文件中对应选项指定所需模式.

*注: 你可以在面向 react JSX 输出时使用 --jsxFactory 选项指定要使用的 JSX 工厂函数 (缺省为 React.createElement)

as 操作符

回想一下怎么书写类型担保:

1
var foo = <foo>bar;

它断定变量 bar 的类型是 foo.
TypeScript 类型担保也采用尖括号, 将它与 JSX 的语法组合无疑会引发解析困难. 因此, TypeScript 不允许 .tsx 文件中出现尖括号式类型担保.

由于以上语法无法在 .tsx 文件中使用, 我们应该选择替代的类型担保运算符: as.
上例很容易用 as 运算符重写.

1
var foo = bar as foo;

as 运算符在 .ts.tsx 文件中都可用, 在行为上与尖括号类型担保风格完全相同.

类型检查

为了理解 JSX 类型检查, 你必须首先理解固有元素和基于值的元素间的区别.
给定 JSX 表达式 <expr />, expr 可以指代一个环境固有的东西 (例如: DOM 环境的一个 divspan 元素), 或一个你创建的自定义组件.
两个原因让这点很重要:

  1. 对 React, 固有元素会以字符串输出 (React.createElement("div")), 而你创建的组件则不会 (React.createElement(MyComponent)).
  2. 传给 JSX 元素的标签属性的类型应该有差别地查找.
    固有元素的标签属性是固定的, 而组件很有可能会规定它们自己的一套标签属性.

TypeScript 采用与 React 一致的习惯区分两者.
一个固有元素总是以小写字母打头, 基于值的元素总是以大写字母打头.

固有元素

我们在一个特殊接口 JSX.IntrisicElements 查找固有元素.
一般, 如果没有指定这个接口, 所有东西, 包括固有元素, 都不会被类型检查.
而如果这个接口存在, 固有元素的名字就会作为一个对象属性在 JSX.IntrinsicElements 接口查找.
例如:

1
2
3
4
5
6
7
8
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}

<foo />; // ok
<bar />; // error

上例, <foo /> 一切正常, 而 <bar /> 没有在 JSX.IntrinsicElements 中声明, 会导致错误.

注: 你也可以如下为 JSX.IntrinsicElements 声明一个”捕获一切”的字符串索引签名:

1
2
3
4
5
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}

基于值的元素

我们直接在当前空间所有标识符中查找基于值的元素.

1
2
3
4
import MyComponent from "./myComponent";

<MyComponent />; // ok
<SomeOtherComponent />; // error

有两种定义基于值的元素的方法:

  1. 函数组件 (FC)
  2. 类组件

因为这两种基于值的元素在一条 JSX 表达式中是无法区分的, TypeScript 首先运用重载解析尝试把这表达式当作函数组件解析. 只要该过程成功, TypeScript 即完成从表达式到对应声明的解析. 如果表达式未能解析为函数组件, TypeScript 会把它当作类组件重试. 而再次失败会让 TypeScript 报错.

函数组件

顾名思义, 函数组件用 JavaScript 函数定义, 它第一个参数是一个 props 对象.
TypeScript 规定其返回值必须能赋值给 JSX.Element.

1
2
3
4
5
6
7
8
9
10
11
12
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 函数, 可以在这里使用函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 {
...
}

注: 函数组件以前叫做无状态函数组件 (SFC). 而在最近的 react 版本中, 函数组件再也不能被认为是无状态的了, 类型 SFC 及其别名 StatelessComponent 随即弃用.

类组件

我们可以定义代表类组件的类型.
但在开始之前, 最好能理解两个新术语: 元素类类型元素实例类型.

给定表达式 <Expr />, 元素类类型Expr 的类型.
我们前面的例子, 如果 MyComponent 是个 ES6 类, 那么类类型是那个类的构造器和静态面.
如果 MyComponent 是一个工厂函数, 类类型就是这个函数.

一旦类类型已知, 实例类型就按类类型的构造函数或调用签名 (无论存在与否) 返回值类型的自适应类型确定.
于是, ES6 类的情形, 实例类型是那个类实例的类型, 工厂函数的情形, 实例类型是函数返回值的类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyComponent {
render() {}
}

// use a construct signature
var myComponent = new MyComponent();

// element class type => MyComponent
// element instance type => { render: () => void }

function MyFactoryFunction() {
return {
render: () => {
}
}
}

// use a call signature
var myComponent = MyFactoryFunction();

// element class type => MyFactoryFunction
// element instance type => { render: () => void }

元素实例类型很有意思, 它必须能赋值给 JSX.ElementClass, 否则会产生错误.
JSX.ElementClass 缺省为 {}, 但它可以被增强, 以限制只有那些符合特定接口的类型才能使用 JSX.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 上属性的类型.

1
2
3
4
5
6
7
8
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 不存在, 就用类元素构造函数或函数组件调用的第一个参数替代.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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" />

元素标签属性类型被用来类型检查 JSX 中的标签属性.
可选和必选属性都受支持.

1
2
3
4
5
6
7
8
9
10
11
12
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

注: 我们不把一个是无效 JavaScript 标识符 (如 data-*) 的属性名未在元素标签属性类型中找到当做一个错误.

此外, 可以用 JSX.IntrinsicAttributes 接口声明由 JSX 框架使用的一般不被组件的 props 或参数使用的额外属性 - 例如 React 的 key. 进一步深入, 泛型类型 JSX.IntrinsicClassAttributes<T> 也可以为类组件 (而非函数组件) 声明同类额外标签属性. 这个类型的泛型参数即对应类实例类型. 在 React 中, 它让类型 Ref<T> 得到 ref 标签属性. 总的来说, 这些接口上所有属性都应该是可选的, 除非你有意让你 JSX 框架的用户为每个标签提供几个标签属性.

扩展操作符也能工作:

1
2
3
4
5
var props = { requiredProp: "bar" };
<foo {...props} />; // ok

var badProps = {};
<foo {...badProps} />; // error

子类型检查

TypeScript 2.3 引入了对孩子的类型检查, 孩子元素标签属性类型的代表被插入的子JSX 表达式的一个特殊属性.
与 TypeScript 如何用 JSX.ElementAttributesProperty 确定下 props 的名字相仿, 它利用 JSX.ElementChildrenAttribute 确定这些属性中间孩子的名字.
JSX.ElementChildrenAttribute 应声明为单一属性.

1
2
3
4
5
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // specify children name to use
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 typings 等声明文件. 它会覆盖来自它们的默认类型,

1
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
29
30
31
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 允许你用花括号 { } 环绕一个表达式将它嵌入两个标签之间.

1
2
3
var a = <div>
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

上面的代码将由于你不能用数字除一个字符串而产生错误.
而输出, 当使用 preserve 模式, 看起来会像:

1
2
3
var a = <div>
{["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React 集成

要结合 JSX 和 React, 你需要使用 React typings.
这些声明为了 React 用途适当地定义了 JSX 名字空间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <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: react 编译器选项使用的确切工厂函数是可配置的. 它可以用 jsxFactory 命令行参数设置, 或用内联注释命令 @jsx 为每个文件单独设置. 比如, 如果你把 jsxFactory 设为 createElement, <div /> 会输出 createElement("div"), 而不是 React.createElement("div").

注释命令版本可能这么使用 (在 TypeScript 2.8 中):

1
2
3
import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

输出:

1
2
const preact = require("preact");
const x = preact.h("div", null);

选择的工厂函数同样会影响在回退到全局 JSX 名字空间前会在哪个局部 JSX 名字空间查找类型检查信息. 如果定义工厂函数 React.createElement (默认), 编译器在检查全局 JSX 前检查 React.JSX. 如果定义工厂函数 h, 编译器会在检查全局 JSX 前检查 h.JSX.

如果这篇文章对您有用,可以考虑打赏:)