JSX
状态: 初稿
译注: 为便于区分, 在本章, 原文 property 译做对象属性, attribute 译做标签属性.
介绍
JSX 是一种类 XML 可嵌入语法.
最终它会被转换成有效的 JavaScript 代码, 转换的语义取决于具体实现.
JSX 随 React 被广为接受, 从此也出现了很多其他实现.
TypeScript 支持嵌入, 类型检查, 直接向 JavaScript 编译 JSX.
基本用法
在使用 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 |
你可以用 --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 环境的一个 div
或 span
元素), 或一个你创建的自定义组件.
两个原因让这点很重要:
- 对 React, 固有元素会以字符串输出 (
React.createElement("div")
), 而你创建的组件则不会 (React.createElement(MyComponent)
). - 传给 JSX 元素的标签属性的类型应该有差别地查找.
固有元素的标签属性是固定的, 而组件很有可能会规定它们自己的一套标签属性.
TypeScript 采用与 React 一致的习惯区分两者.
一个固有元素总是以小写字母打头, 基于值的元素总是以大写字母打头.
固有元素
我们在一个特殊接口 JSX.IntrisicElements
查找固有元素.
一般, 如果没有指定这个接口, 所有东西, 包括固有元素, 都不会被类型检查.
而如果这个接口存在, 固有元素的名字就会作为一个对象属性在 JSX.IntrinsicElements
接口查找.
例如:
1 | declare namespace JSX { |
上例, <foo />
一切正常, 而 <bar />
没有在 JSX.IntrinsicElements
中声明, 会导致错误.
注: 你也可以如下为
JSX.IntrinsicElements
声明一个”捕获一切”的字符串索引签名:
1 | declare namespace JSX { |
基于值的元素
我们直接在当前空间所有标识符中查找基于值的元素.
1 | import MyComponent from "./myComponent"; |
有两种定义基于值的元素的方法:
- 函数组件 (FC)
- 类组件
因为这两种基于值的元素在一条 JSX 表达式中是无法区分的, TypeScript 首先运用重载解析尝试把这表达式当作函数组件解析. 只要该过程成功, TypeScript 即完成从表达式到对应声明的解析. 如果表达式未能解析为函数组件, TypeScript 会把它当作类组件重试. 而再次失败会让 TypeScript 报错.
函数组件
顾名思义, 函数组件用 JavaScript 函数定义, 它第一个参数是一个 props
对象.
TypeScript 规定其返回值必须能赋值给 JSX.Element
.
1 | interface FooProp { |
由于函数组件就是 JavaScript 函数, 可以在这里使用函数重载:
1 | interface ClickableProps { |
注: 函数组件以前叫做无状态函数组件 (SFC). 而在最近的 react 版本中, 函数组件再也不能被认为是无状态的了, 类型
SFC
及其别名StatelessComponent
随即弃用.
类组件
我们可以定义代表类组件的类型.
但在开始之前, 最好能理解两个新术语: 元素类类型和元素实例类型.
给定表达式 <Expr />
, 元素类类型 是 Expr
的类型.
我们前面的例子, 如果 MyComponent
是个 ES6 类, 那么类类型是那个类的构造器和静态面.
如果 MyComponent
是一个工厂函数, 类类型就是这个函数.
一旦类类型已知, 实例类型就按类类型的构造函数或调用签名 (无论存在与否) 返回值类型的自适应类型确定.
于是, ES6 类的情形, 实例类型是那个类实例的类型, 工厂函数的情形, 实例类型是函数返回值的类型.
1 | class MyComponent { |
元素实例类型很有意思, 它必须能赋值给 JSX.ElementClass
, 否则会产生错误.JSX.ElementClass
缺省为 {}
, 但它可以被增强, 以限制只有那些符合特定接口的类型才能使用 JSX.
1 | declare namespace JSX { |
标签属性类型检查
类型检查标签属性的第一步是判断元素标签属性类型.
该过程在固有和基于值的元素间稍有不同.
对固有元素, 它是接口 JSX.IntrinsicElements
上属性的类型.
1 | declare namespace JSX { |
至于基于值的类型, 则要复杂一点.
它由已知的元素实例类型上属性的类型所确定.
将使用哪些属性又由 JSX.ElementAttributesProperty
接口所确定.
它应该声明为单一属性.
以后可以使用这个属性的名字.
自 TypeScript 2.8 开始, 如果 JSX.ElementAttributesProperty
不存在, 就用类元素构造函数或函数组件调用的第一个参数替代.
1 | declare namespace JSX { |
元素标签属性类型被用来类型检查 JSX 中的标签属性.
可选和必选属性都受支持.
1 | declare namespace JSX { |
注: 我们不把一个是无效 JavaScript 标识符 (如
data-*
) 的属性名未在元素标签属性类型中找到当做一个错误.
此外, 可以用 JSX.IntrinsicAttributes
接口声明由 JSX 框架使用的一般不被组件的 props 或参数使用的额外属性 - 例如 React 的 key
. 进一步深入, 泛型类型 JSX.IntrinsicClassAttributes<T>
也可以为类组件 (而非函数组件) 声明同类额外标签属性. 这个类型的泛型参数即对应类实例类型. 在 React 中, 它让类型 Ref<T>
得到 ref
标签属性. 总的来说, 这些接口上所有属性都应该是可选的, 除非你有意让你 JSX 框架的用户为每个标签提供几个标签属性.
扩展操作符也能工作:
1 | var props = { requiredProp: "bar" }; |
子类型检查
TypeScript 2.3 引入了对孩子的类型检查, 孩子是元素标签属性类型的代表被插入的子JSX 表达式的一个特殊属性.
与 TypeScript 如何用 JSX.ElementAttributesProperty
确定下 props 的名字相仿, 它利用 JSX.ElementChildrenAttribute
确定这些属性中间孩子的名字.JSX.ElementChildrenAttribute
应声明为单一属性.
1 | declare namespace JSX { |
1 | <div> |
你可以与其他任何标签属性一样指定孩子的类型. 如果你使用如 React typings 等声明文件. 它会覆盖来自它们的默认类型,
1 | interface PropsType { |
JSX 结果类型
默认情况下, JSX 表达式结果的类型是 any
.
你可以通过规定 JSX.Element
接口自定义它的类型.
然而, 我们无法从该接口获取关于 JSX 元素, 标签属性, 或孩子的类型信息.
它是一个黑盒子.
内嵌表达式
JSX 允许你用花括号 { }
环绕一个表达式将它嵌入两个标签之间.
1 | var a = <div> |
上面的代码将由于你不能用数字除一个字符串而产生错误.
而输出, 当使用 preserve
模式, 看起来会像:
1 | var a = <div> |
React 集成
要结合 JSX 和 React, 你需要使用 React typings.
这些声明为了 React 用途适当地定义了 JSX
名字空间.
1 | /// <reference path="react.d.ts" /> |
工厂函数
由 jsx: react
编译器选项使用的确切工厂函数是可配置的. 它可以用 jsxFactory
命令行参数设置, 或用内联注释命令 @jsx
为每个文件单独设置. 比如, 如果你把 jsxFactory
设为 createElement
, <div />
会输出 createElement("div")
, 而不是 React.createElement("div")
.
注释命令版本可能这么使用 (在 TypeScript 2.8 中):
1 | import preact = require("preact"); |
输出:
1 | const preact = require("preact"); |
选择的工厂函数同样会影响在回退到全局 JSX
名字空间前会在哪个局部 JSX
名字空间查找类型检查信息. 如果定义工厂函数 React.createElement
(默认), 编译器在检查全局 JSX
前检查 React.JSX
. 如果定义工厂函数 h
, 编译器会在检查全局 JSX
前检查 h.JSX
.