JSX 类型支持
本教程共 80 篇 · 第 75 篇 · 更新于 2026-08-10 · 约 14 分钟阅读
本节目标:搞懂 TypeScript 怎么给 JSX 提供类型检查——四种 JSX 模式的区别、如何标注原生 HTML 元素的类型、怎样给自定义组件写 Props 类型。不深入框架内部,只聚焦 TS 层面的类型标注。
JSX 不是 JS 的一部分
JSX 语法(<div>hello</div>)不是 ECMAScript 标准,它是编译器的扩展语法。TypeScript 支持 JSX,但需要你告诉它”编译成什么”——这个选择由 tsconfig.json 里的 jsx 选项控制。
四种 JSX 模式
TS 7.0 保留了四种 JSX 模式(移除了旧模式,只留下现代选项):
| 模式 | jsx 值 | 输出行为 | 适用场景 |
|---|---|---|---|
| preserve | "preserve" | 保留 JSX 不变,输出 .jsx | 交给 Babel 等后续工具处理 |
| react-jsx | "react-jsx" | 自动导入 jsx 函数,不引入 React | React 17+ 的新 JSX 转换 |
| react-jsxdev | "react-jsxdev" | 同 react-jsx,附加调试信息 | React 17+ 开发模式 |
| react | "react" | 转换为 React.createElement 调用 | React 16 及更早版本(传统转换) |
TS 7.0 移除了 react-native 模式。React Native 项目现在统一用 react-jsx。
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
Note
jsx选项只控制编译输出格式,类型检查的行为和模式无关——无论选哪个,TS 的类型检查逻辑是一样的。
固有元素:JSX.IntrinsicElements
TypeScript 怎么知道你写的 <div> 有哪些合法属性?答案是一个特殊的全局接口:JSX.IntrinsicElements。
// JSX 命名空间中的接口(框架的 .d.ts 文件里会声明)
declare namespace JSX {
interface IntrinsicElements {
div: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
span: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLSpanElement>,
HTMLSpanElement
>;
a: React.DetailedHTMLProps<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
// ... 所有 HTML 元素
}
}
这个接口里的每一条,都定义了对应 HTML 标签的属性类型。你写 <a href="/"> 时,TS 通过 JSX.IntrinsicElements["a"] 找到属性定义,然后检查你传的 href 对不对。
属性类型检查示例
// ✅ href 和 onClick 都在 a 元素的类型定义中
<a href="/home" onClick={(e) => console.log(e)}>
首页
</a>
// ❌ href 的值应该是 string,不是 number
<a href={42}>首页</a>
// TS 报错:Type 'number' is not assignable to type 'string'
// ❌ foo 不是 a 元素的合法属性
<a foo="bar">首页</a>
// TS 报错:Property 'foo' does not exist on type '...'
这就是 JSX 类型检查的核心价值:写标签时就能发现属性传错的 bug。
自定义组件:Props 类型
自定义组件的类型检查,本质上就是函数参数的类型检查。JSX 的 <MyComponent prop1={...} /> 会被编译器理解成函数调用,所有属性打包成一个对象传给函数。
React 组件(函数式)
interface ButtonProps {
label: string;
disabled?: boolean;
onClick: () => void;
}
function Button({ label, disabled = false, onClick }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{label}
</button>
);
}
// 使用
<Button label="提交" onClick={() => console.log("click")} />
<Button label="取消" disabled onClick={() => {}} />
// ❌ label 必填但没传
<Button onClick={() => {}} />
// TS 报错:Property 'label' is missing
// ❌ disabled 应该是 boolean
<Button label="确认" disabled="yes" onClick={() => {}} />
// TS 报错:Type 'string' is not assignable to type 'boolean'
React 组件(类组件)
interface CounterProps {
initial: number;
step?: number;
}
interface CounterState {
count: number;
}
class Counter extends React.Component<CounterProps, CounterState> {
state: CounterState = {
count: this.props.initial,
};
render() {
const { step = 1 } = this.props;
return (
<div>
<span>{this.state.count}</span>
<button onClick={() => this.setState({ count: this.state.count + step })}>
+
</button>
</div>
);
}
}
<Counter initial={0} step={5} />;
React.Component<P, S> 的第一个泛型是 Props 类型,第二个是 State 类型。TS 会检查 <Counter> 标签上有没有传对 Props。
Vue 3 组件(defineComponent + defineProps)
Vue 3 有自己的一套类型标注方式,但原理一样:
<script setup lang="ts">
interface Props {
message: string;
count?: number;
}
const props = defineProps<Props>();
// props.message: string
// props.count: number | undefined
</script>
<template>
<div>{{ message }} - {{ count }}</div>
</template>
Vue 的 defineProps<Props>() 在编译期把泛型参数翻译成运行时的 props 声明,同时保留类型信息给 IDE。
SolidJS 组件
Solid 的函数组件 Props 类型标注和 React 几乎一样:
interface TodoItemProps {
text: string;
completed: boolean;
onToggle: () => void;
}
function TodoItem(props: TodoItemProps) {
return (
<div class={props.completed ? "done" : ""}>
<input type="checkbox" checked={props.completed} onChange={props.onToggle} />
<span>{props.text}</span>
</div>
);
}
框架层面写法略有不同,但 TS 对它们的类型检查逻辑一致:把 JSX 的属性看作一个对象字面量,用组件函数的参数类型去校验。
children 的类型
children 是所有 JSX 框架都无法绕开的类型问题。在 React 中:
interface CardProps {
title: string;
children?: React.ReactNode; // React 18+
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
<Card title="通知">
<p>你有一条新消息</p>
<button>查看</button>
</Card>
React.ReactNode 是 React 里最宽泛的 children 类型——它接受 JSX 元素、字符串、数字、null、undefined、数组等任何可以作为子节点的内容。
如果你只想接受特定类型:
interface ListProps {
items: string[];
renderItem?: (item: string) => React.ReactNode;
}
function List({ items, renderItem }: ListProps) {
return (
<ul>
{items.map((item) => (
<li key={item}>{renderItem ? renderItem(item) : item}</li>
))}
</ul>
);
}
事件处理器的类型
React 为每种原生事件提供了对应的类型:
interface InputProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
function Input({ value, onChange, onKeyDown }: InputProps) {
return <input value={value} onChange={onChange} onKeyDown={onKeyDown} />;
}
常用的事件类型:
| 事件类型 | 适用场景 |
|---|---|
React.ChangeEvent<T> | 表单输入变化 |
React.MouseEvent<T> | 鼠标点击/移动 |
React.KeyboardEvent<T> | 键盘事件 |
React.FormEvent<T> | 表单提交 |
React.FocusEvent<T> | 聚焦/失焦 |
泛型参数 T 是 HTML 元素类型,比如 HTMLInputElement、HTMLButtonElement、HTMLDivElement。写对了类型,IDE 里 .currentTarget.value 等属性就有代码补全。
在 Vue 中使用 TSX
Vue 3 也支持 TSX 写法(需要 @vitejs/plugin-vue-jsx 或 @vue/babel-plugin-jsx):
import { defineComponent } from "vue";
export default defineComponent({
props: {
message: { type: String, required: true },
},
setup(props) {
return () => <div>{props.message}</div>;
},
});
Vue 的 TSX 类型底层依赖 JSX.IntrinsicElements,和 React 共享同一套 TS 基础设施。区别在于 Vue 的 IntrinsicElements 里定义的是 Vue 风格的属性(比如 v-model 等指令的类型)。
框架适配差异:一张表
| 特性 | React | Vue 3 | Solid |
|---|---|---|---|
| Props 标注 | interface Props { ... } | defineProps<Props>() | props: Props 参数 |
| children 类型 | React.ReactNode | slots 机制 | JSX.Element |
| 事件类型 | React.ChangeEvent<T> | emit 声明 | onChange: (e: Event) => void |
| 类组件泛型 | React.Component<P, S> | 无(Vue3 推荐组合式) | 无 |
| JSX 模式 | react-jsx | preserve / react-jsx | preserve / react-jsx |
| 类型定义来源 | @types/react | 编译期内置 | 编译期内置 |
Note不管你用哪个框架,TS 对 JSX 的类型检查机制是一样的:检查标签名是否存在于
JSX.IntrinsicElements或组件函数中,检查属性是否匹配参数类型。不同框架只是在具体的类型定义上有差异。
小结
jsx选项控制编译输出格式,不影响类型检查行为JSX.IntrinsicElements定义了所有原生 HTML 标签的属性类型- 自定义组件的 Props 类型就是组件的参数类型——TS 用函数参数校验去检查 JSX 属性
- children 的类型在不同框架中不同:React 用
React.ReactNode,Vue 用 slots - 事件类型在 React 中有完整的泛型体系:
React.ChangeEvent<T>、React.MouseEvent<T>等 - Vue 3 和 Solid 的 JSX 底层同样依赖
JSX.IntrinsicElements,只是具体类型定义不同