◆

tsconfig jsx 属性配置详解

2023-02-02 01:370

jsx 属性配置

TypeScript 具有三种 JSX 模式:preserve,react 和 react-native

模式输入输出输出文件扩展名
preserve<div/><div/>.jsx
react<div/>React.createElement("div").js
react-native<div/><div/>.js

上述这些模式只在代码生成阶段起作用 - 类型检查并不受影响。在 preserve 模式下生成代码中会保留 JSX 以供后续的转换操作使用(比如:Babel)

后来新增的两种模式

TypeScript 4.1 又加了两个值,对应 React 17 引入的新 JSX 运行时:

模式输入输出
react-jsx<div/>_jsx("div", {}),并自动 import 运行时
react-jsxdev<div/>_jsxDEV("div", {}, ...),带调试信息

关键差别是不再需要在每个文件顶部 import React。老的 react 模式产出 React.createElement(...),React 这个标识符必须在作用域里,所以哪怕文件里一个 React API 都没用到,也得写一行 import React from 'react';忘了就报 React refers to a UMD global。

react-jsx 模式下编译器自己注入导入:

// 源码:不需要 import React
export const App = () => <div className="app">hi</div>;
// react-jsx 产出
import { jsx as _jsx } from 'react/jsx-runtime';
export const App = () => _jsx('div', { className: 'app', children: 'hi' });

react-jsxdev 是它的开发版,额外传入文件名、行号等信息,React 的错误提示能定位到具体源码位置。常规配置是 react-jsx,让打包器在 dev 环境切到 react-jsxdev(Vite 的 React 插件会自动处理)。

该选哪个

判断依据是「谁负责最终的 JSX 转换」:

  • 交给打包器(Vite / esbuild / Babel / SWC):选 preserve。tsc 只做类型检查,JSX 原样留给下游。Vite 项目基本都是这个配置,配合 noEmit: true。
  • tsc 自己产出可运行 JS:React 17+ 选 react-jsx,React 16 及以下选 react。
  • React Native:选 react-native。它和 preserve 的产出内容一样,只有扩展名不同(.js 而非 .jsx),因为 RN 的 Metro 打包器默认只解析 .js。

注意 preserve 模式下产出文件是 .jsx,如果下游工具链只认 .js,就得改工具链配置或者改用 react-native 模式。

jsxImportSource:给非 React 库用

react-jsx 默认从 react/jsx-runtime 导入,用别的库(Preact、Solid、Vue JSX)时用 jsxImportSource 换掉这个来源:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

这样导入会变成 from 'preact/jsx-runtime'。也可以在单个文件里用注释覆盖,混用多个 JSX 库时有用:

/** @jsxImportSource preact */
export const App = () => <div />;

jsxFactory 与 jsxFragmentFactory

这两个只在 jsx: "react"(经典模式)下生效,用来改工厂函数名:

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

配了之后 <div/> 编译成 h("div", null),<></> 编译成 h(Fragment, null)。老的 Preact / Vue JSX 项目常见这套配置。同样支持文件级注释 /** @jsx h */ 和 /** @jsxFrag Fragment */。

jsxFragmentFactory 是 TS 4.0 才加的,在那之前 <></> 只能编译成 React.Fragment,用 Preact 时要么手写 <Fragment> 要么打补丁。

JSX 的类型是怎么来的

这块和 jsx 配置项无关但常被一起问到。JSX 元素的类型检查依赖一个叫 JSX 的命名空间,React 的类型包里声明了它:

// 简化示意
declare namespace JSX {
  // 决定 <div/> 这类表达式的类型
  interface Element extends React.ReactElement<any, any> {}
  // 决定哪些小写标签合法,以及各自接受什么属性
  interface IntrinsicElements {
    div: React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLDivElement>,
      HTMLDivElement
    >;
    // ...
  }
}

所以「<div> 上写 clasName 会报错」这个能力来自 IntrinsicElements,不是 tsc 内置的 HTML 知识。想让自定义元素通过检查,就往这个接口里补声明:

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'my-widget': { size?: number; children?: React.ReactNode };
    }
  }
}

TypeScript 5.1 之后 React 类型包把 JSX 命名空间挪进了 React.JSX,全局的那份仍然保留以兼容旧代码。写扩展声明时两个位置都可以,但一个项目里最好统一。

文件扩展名必须是 .tsx

在 .ts 文件里写 JSX 会报 Cannot use JSX unless the '--jsx' flag is provided,即使配了 jsx 选项也一样。原因是 .ts 里的 <T>expr 是类型断言语法,和 JSX 标签有歧义,TypeScript 干脆规定 .ts 不解析 JSX。

这也意味着 .tsx 里不能用尖括号断言,只能用 as:

// .tsx 中报错
const el = <HTMLDivElement>document.querySelector('div');
// 正确
const el = document.querySelector('div') as HTMLDivElement;

泛型箭头函数在 .tsx 里也有歧义,<T>(x: T) => x 会被当成 JSX。惯用的绕法是加个逗号或约束:

const identity = <T,>(x: T) => x;
// 或
const identity = <T extends unknown>(x: T) => x;
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEtsconfig jsx 属性配置详解加载中