JSX_TypeScript笔记17

一.基本用法

TypeScript 也支持JSX,除了能够像Babel一样把 JSX 编译成 JavaScript 外,还提供了类型检查

只需 2 步,即可使用 TypeScript 写 JSX:

  • 源码文件用.tsx扩展名

  • 开启--jsx选项

此外,TypeScript 提供了 3 种 JSX 处理模式,分别对应不同的代码生成规则:

Mode Input Output Output File Extension
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js
react-native <div /> <div /> .js

也就是说:

  • preserve:生成.jsx文件,但保留 JSX 语法不转换,交给后续构建环节(如Babel)处理

  • react:生成.js文件,将 JSX 语法转换成React.createElement

  • react-native:生成.js文件,但保留 JSX 语法不转换

这些模式通过--jsx选项来指定,默认"preserve",只影响代码生成,并不影响类型检查(例如--jsx "preserve"要求不转换,但仍会对 JSX 进行类型检查)

具体使用上,JSX 语法完全保持一致,唯一需要注意的是类型断言

类型断言

在 JSX 中只能用as type(尖括号语法与 JSX 语法冲突)

let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;

.tsx文件中会引发报错:

JSX element ‘string’ has no corresponding closing tag.

‘</’ expected.

由于语法冲突,<string>someValue中的类型断言部分(<string>)被当成 JSX 元素了。所以在.tsx中只能使用as type形式的类型断言:

// as type
let strLength: number = (someValue as string).length;

P.S.关于 TypeScript 类型断言的更多信息,见三.类型断言

二.元素类型

对于一个 JSX 表达式<expr />expr可以是环境中的固有元素(intrinsic element,即内置组件,比如 DOM 环境中的divspan),也可以是基于值的元素(value-based element),即自定义组件。两种元素的区别在于:

  • 生成的目标代码不同

    React 中,固有元素会生成字符串(比如React.createElement("div")),而自定义组件不会(比如React.createElement(MyComponent)

  • 元素属性(即Props)类型的查找方式不同

    固有元素的属性是已知的,而自定义组件可能想要指定自己的属性集

形式上,要求自定义组件必须首字母大写,以此区分两种 JSX 元素

P.S.实际上,固有元素/基于值的元素与内置组件/自定义组件说的是一回事,对 TypeScript 编译器而言,内置组件的类型已知,称之为固有元素,自定义组件的类型与组件声明(值)有关,称之为基于值的元素

固有元素

固有元素的类型从JSX.IntrinsicElements接口上查找,如果没有声明该接口,那么所有固有元素都不做类型检查,如果声明了,就在JSX.IntrinsicElements上查找对应的属性,作为类型检查的依据:

declare namespace JSX {
  interface IntrinsicElements {
    foo: any
  }
}

// 正确
<foo />;
// 错误 Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;

当然,也可以配合索引签名允许使用未知的内置组件:

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

// 正确
<bar />;

好处是将来扩展支持新内置组件后,不需要立即修改类型声明,代价是失去了白名单的严格校验

基于值的元素

基于值的元素直接从作用域里找对应标识符,例如:

import MyComponent from "./myComponent";

// 正确
<MyComponent />
// 错误 Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />

共有 2 种基于值的元素:

  • 无状态的函数式组件(Stateless Functional Component,所谓 SFC)

  • 类组件(Class Component)

二者单从 JSX 表达式的形式上区分不开,因此先当作 SFC 按照函数重载去尝试解析,解析失败才当类组件处理,还失败就报错

无状态的函数式组件

形式上是个普通函数,要求第一个参数是props对象,返回类型是JSX.Element(或其子类型),例如:

function Welcome(props: { name: string }) {
  return <h1>Hello, {props.name}</h1>;
}

同样地,函数重载仍然适用:

function Welcome(props: { content: JSX.Element[] | JSX.Element });
function Welcome(props: { name: string });
function Welcome(props: any) {
  <h1>Hello, {props.name}</h1>;
}

<div>
  <Welcome name="Lily" />
  <Welcome content={<span>Hello</span>} />
</div>

P.S.JSX.Element类型声明来自@types/react

类组件

类组件则继承自React.Component,与 JavaScript 版没什么区别:

class WelcomeClass extends React.Component {
  render() {
    return <h1>Hello, there.</h1>;
  }
}

<WelcomeClass />

类似于 Class 的双重类型含义,对于 JSX 表达式<Expr />,类组件的类型分为 2 部分:

  • 元素类类型(element class type):Expr的类型,即typeof WelcomeClass

  • 元素实例类型(element instance type):Expr类实例的类型,即{ render: () => JSX.Element }

例如:

// 元素类类型
let elementClassType: typeof WelcomeClass;
new elementClassType();
// 元素实例类型
let elementInstanceType: WelcomeClass;
elementInstanceType.render();

要求元素实例类型必须是JSX.ElementClass的子类型,默认JSX.ElementClass类型为{},在 React 里则限定必须具有render方法:

namespace JSX {
  interface ElementClass extends React.Component<any> {
    render(): React.ReactNode;
  }
}

(摘自DefinitelyTyped/types/react/index.d.ts

否则报错:

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<div>
  {/* 错误 JSX element type 'NotAValidComponent' is not a constructor function for JSX elements. */}
  <NotAValidComponent />
  {/* 错误 JSX element type '{}' is not a constructor function for JSX elements. */}
  <NotAValidFactoryFunction />
</div>

三.属性类型

属性检查首先要确定元素属性类型(element attributes type),固有元素和基于值的元素在属性类型上存在些许差异:

  • 固有元素的属性类型:JSX.IntrinsicElements上对应属性的类型

  • 基于值的元素属性类型:元素实例类型上特定属性类型上对应属性的类型,这个特定属性通过JSX.ElementAttributesProperty指定

P.S.如果未声明JSX.ElementAttributesProperty,就取组件类构造函数或 SFC 第一个参数的类型

具体的,固有元素属性以ahref为例:

namespace JSX {
  interface IntrinsicElements {
    // 声明各个固有元素,及其属性类型
    a: {
      download?: any;
      href?: string;
      hrefLang?: string;
      media?: string;
      rel?: string;
      target?: string;
      type?: string;
      referrerPolicy?: string;
    }
  }
}

// 元素属性类型为 { href?: string }
<a href="">链接</a>

基于值的元素属性例如:

namespace JSX {
  // 指定特定属性名为 props
  interface ElementAttributesProperty { props: {}; }
}

class MyComponent extends React.Component {
  // 声明属性类型
  props: {
    foo?: string;
  }
}
// 元素属性类型为 { foo?: string }
<MyComponent foo="bar" />

可选属性、展开运算符等也同样适用,例如:

class MyComponent extends React.Component {
  // 声明属性类型
  props: {
    requiredProp: string;
    optionalProp?: string;
  }
}

const props = { optionalProp: 'optional' };
// 正确
<MyComponent { ...props } requiredProp="required" />

P.S.另外,JSX 框架可以通过JSX.IntrinsicAttributes指定框架所需的额外属性,比如 React 里的key,具体见Attribute type checking

P.S.特殊的,属性校验只针对属性名为合法 JavaScript 标识符的属性data-*之类的不做校验

子组件类型检查

子组件的类型来自元素属性类型上的children属性,类似于用ElementAttributesProperty指定props,这里用JSX.ElementChildrenAttribute来指定children

namespace JSX {
  // 指定特定属性名为 children
  interface ElementChildrenAttribute { children: {}; }
}

const Wrapper = (props) => (
  <div>
    {props.children}
  </div>
);
<Wrapper>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</Wrapper>

children指定类型的方式与普通属性类似:

interface PropsType {
  children: JSX.Element
  name: string
}
class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

<Component name="hello">
  <h1>Hello World</h1>
</Component>

子组件类型不匹配会报错:

// 错误 Type '{ children: Element[]; name: string; }' is not assignable to type 'Readonly<PropsType>'.
<Component name="hello">
  <h1>Hello World</h1>
  <h1>Hi</h1>
</Component>

四.结果类型

默认情况下,一个 JSX 表达式的结果类型是any

// a 的类型为 any
let a = <a href="" />;
a = {};

可以通过JSX.Element来指定,例如 React 中:

let a = <a href="" />;
// 错误 Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};

对应的类型声明类似于:

namespace JSX {
  interface Element<T, P> {
    type: T;
    props: P;
    key: string | number | null;
  }
}

P.S.React 里具体的 JSX 元素类型声明见DefinitelyTyped/types/react/index.d.ts

五.嵌入的表达式

JSX 允许在标签内通过花括号语法({ })插入表达式:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

(摘自Embedding Expressions in JSX

TypeScript 同样支持,并且能够对嵌入的表达式做类型检查:

const a = <div>
  {/* 错误 The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. */}
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

六.结合 React

引入React 类型定义之后,很容易描述 Props 的类型:

interface WelcomeProps {
  name: string;
}
// 将 Props 的类型作为第一个类型参数传入
class WelcomeClass extends React.Component<WelcomeProps, {}> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
// 错误 Property 'name' is missing in type '{}' but required in type 'Readonly<WelcomeProps>'.
let errorCase = <WelcomeClass />;
let correctCase = <WelcomeClass name="Lily" />;

P.S.关于类型参数及泛型的更多信息,见二.类型变量

工厂函数

React 模式(--jsx react)下,可以配置具体使用的 JSX 元素工厂方法,有 2 种方式:

  • --jsxFactory选项:项目级配置

  • 内联@jsx注释指令:文件级配置

默认为--jsxFactory "React.createElement",将 JSX 标签转换为工厂方法调用:

const div = <div />;
// 编译结果
var div = React.createElement("div", null);

Preact里对应的 JSX 元素工厂方法为h

/* @jsx preact.h */
import * as preact from "preact";
<div />;
// 或者
/* @jsx h */
import { h } from "preact";
<div />;

P.S.注意,@jsx注释指令必须出现在文件首行,其余位置无效

编译结果分别为:

/* @jsx preact.h */
var preact = require("preact");
preact.h("div", null);
// 或者
/* @jsx h */
var preact_1 = require("preact");
preact_1.h("div", null);

P.S.另外,工厂方法配置还会影响 JSX 命名空间的查找,比如默认--jsxFactory React.createElement的话,优先查找React.JSX,接下来才看全局JSX命名空间,如果指定--jsxFactory h,就优先查找h.JSX

七.总结

TypeScript 中 JSX 的类型支持分为元素类型、属性类型和结果类型 3 部分,如下图:

tsx

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code