tsconfig jsx 属性配置详解
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;

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