九、JSX

JSX 是一种可嵌入的类似XML的语法。它旨在被转换为有效的JavaScript,尽管这种转换的语义是具体实施的。JSX随着React 框架的流行而兴起,但后来也有了其他的实现。TypeScript支持嵌入、类型检查,以及直接将JSX编译为JavaScript。

9. 1 基本用法

为了使用JSX,你必须做两件事。

  1. .tsx 扩展名来命名你的文件
  2. 启用 jsx 选项

TypeScript有三种JSX模式:preserve, reactreact-native。这些模式只影响生成阶段 - 类型检查不受影响。 preserve 模式将保留 JSX 作为输出的一部分,以便被另一个转换步骤(例如 Babel)进一步消耗。此外,输出将有一个 .jsx 文件扩展名。react模式将发出 React.createElement,在使用前不需要经过JSX转换,而且输出将有一个 .js 文件扩展名。react-native 模式相当于保留模式,它保留了所有的JSX,但输出将有一个 .js 文件扩展名。

ModeInputOutputOutput 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

You can specify this mode using either the jsx command line flag or the corresponding option jsx in your tsconfig.json file.

你可以使用 jsx 命令行标志或你的 tsconfig.json 文件中的相应选项 jsx 指定这种模式。

*注意:你可以用 jsxFactory 选项指定针对 react JSX 生成 JS 时使用的JSX工厂函数(默认为React.createElement)。

9.2 as操作符

回忆一下如何编写类型断言。

const foo = <foo>bar;

这断言变量 bar具有foo类型。由于TypeScript也使用角括号进行类型断言,将其与JSX的语法相结合会带来某些解析困难。因此,TypeScript不允许在 .tsx 文件中使用角括号类型断言。

由于上述语法不能在 .tsx 文件中使用,应该使用一个替代的类型断言操作符:as。这个例子可以很容易地用 as 操作符重写。

const foo = bar as foo;

as 操作符在 .ts.tsx 文件中都可用,并且在行为上与角括号式断言风格相同。

9.3 类型检查

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

  • 对于React来说,内在元素是以字符串的形式发出的(React.createElement("div")),而你创建的组件则不是(React.createElement(MyComponent))。
  • 在JSX元素中传递的属性类型应该被不同地查找。元素的内在属性应该是已知的,而组件可能想要指定他们自己的属性集。

TypeScript使用与React相同的约定 来区分这些。一个内在的元素总是以小写字母开始,而一个基于价值的元素总是以大写字母开始。

9.4 内在元素

内在元素在特殊接口 JSX.IntrinsicElements 上被查询到。默认情况下,如果没有指定这个接口,那么什么都可以,内在元素将不会被类型检查。然而,如果这个接口存在,那么内在元素的名称将作为JSX.IntrinsicElements接口上的一个属性被查询。比如说。

declare namespace JSX {
interface IntrinsicElements {
foo: any;
}
}
<foo />; // 正确
<bar />; // 错误

在上面的例子中,<foo />可以正常工作,但<bar />会导致一个错误,因为它没有被指定在JSX.IntrinsicElements上。

注意:你也可以在JWX.IntrinsicElements上指定一个全面的字符串索引器,如下所示:

declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}

9.5 基于值的元素

基于值的元素只是通过范围内的标识符进行查询。

import MyComponent from "./myComponent";
<MyComponent />; // 正确
<SomeOtherComponent />; // 错误

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

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

因为这两类基于值的元素在JSX表达式中是无法区分的,首先TS尝试使用重载解析将表达式解析为一个函数组件。如果这个过程成功了,那么TS就完成了将表达式解析为它的声明。如果该值不能被解析为一个函数组件,那么TS将尝试将其解析为一个类组件。如果失败了,TS将报告一个错误。

9.5.1 函数组件

顾名思义,该组件被定义为一个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 {
// ...
}

注意:函数组件以前被称为无状态函数组件(SFC)。由于Function Components在最近的react版本中不再被认为是无状态的,SFC类型和它的别名StatelessComponent被废弃了。

9.5.2 类组件

定义一个类组件的类型是可能的。然而,要做到这一点,最好理解两个新术语:元素类类型和元素实例类型。

给定<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 />; // 正确
<MyFactoryFunction />; // 正确

class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<NotAValidComponent />; // 错误
<NotAValidFactoryFunction />; // 错误

9.6 属性类型检查

类型检查属性的第一步是确定元素属性类型。这在内在元素和基于值的元素之间略有不同。

对于内在元素,它是 JSX.IntrinsicElements 上的属性类型。

declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean };
}
}
// 'foo'的元素属性类型是'{bar?: boolean}'
<foo bar />;

元素属性类型是用来对JSX中的属性进行类型检查的。支持可选和必需的属性。

declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number };
}
}
<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' 不是一个有效的属性标识

注意:如果一个属性名称不是一个有效的JS标识符(如data-*属性),如果在元素属性类型中找不到它,则不被认为是一个错误。

此外,JSX.IntrinsicAttributes接口可以用来指定JSX框架使用的额外属性,这些属性一般不会被组件的道具或参数使用–例如React中的key。进一步专门化,通用的 JSX.IntrinsicClassAttributes<T>类型也可以用来为类组件(而不是函数组件)指定同种额外属性。在这种类型中,通用参数与类的实例类型相对应。在React中,这被用来允许Ref<T>类型的ref属性。一般来说,这些接口上的所有属性都应该是可选的,除非你打算让你的JSX框架的用户需要在每个标签上提供一些属性。

展开运算符也能正常工作:

const props = { requiredProp: "bar" };
<foo {...props} />; // 正确
const badProps = {};
<foo {...badProps} />; // 错误

9.7 子类型检查

在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 名称
}
}
<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>
)
}
}

// 正确
<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>

9.8 JSX的结果类型

默认情况下,JSX表达式的结果被打造成 any 类型。你可以通过指定 JSX.Element 接口来定制类型。然而,不可能从这个接口中检索到关于JSX的元素、属性或孩子的类型信息。它是一个黑盒子。

9.9 嵌入表达式

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>
);

9.10 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} />; // 错误

9.10.1 配置JSX

有多个编译器标志可以用来定制你的JSX,它们既可以作为编译器标志,也可以通过内联的每个文件实用程序发挥作用。要了解更多信息,请看他们的tsconfig参考页:

特别声明: 本文转自 古艺散人老师 ,如有需要可前往原文预览查看。