# JSX

JSX 是一种可嵌入的类 XML 语法。它旨在转换为有效的 JavaScript,尽管该转换的语义是特定于实现的。JSX 随着 React 框架而流行起来,但后来也看到了其他实现。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。

# 基本用法

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

  • 使用 .tsx扩展名命名您的文件
  • 启用 jsx 选项

TypeScript 附带三种 JSX 模式:preservereactreact-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 表达式 &lt;expr /&gt;expr可以指代环境固有的东西(例如 DOM 环境中的 divspan),也可以指您创建的自定义组件。这很重要,原因有两个:

  • 对于 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

在上面的示例中,&lt;foo /&gt;可以正常工作,但 &lt;bar /&gt;将导致错误,因为它没有在 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已被弃用。

# 类组件

可以定义类组件的类型。然而,这样做最好理解两个新术语:元素类类型和元素实例类型。

给定 &lt;Expr /&gt;,元素类类型是 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&gt;类型也可用于为类组件(而不是函数组件)指定相同类型的额外属性。在这种类型中,泛型参数对应于类实例类型。在 React 中,这用于允许 Ref<T&gt;类型的 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
Last Updated: 5/25/2023, 2:35:11 PM