自定义React中JSX的Element类型
在react-vue中,我定义了一种setup式的函数组件,这种组件返回的是一个render函数,因此在tsx中需要实现一种新的JSX.Element,否则会出现函数组件返回值类型冲突的问题。本文主要探究如何解决这个问题。
先过一下官方文档
- JSX - TypeScript文档,先看一下文档,了解TypeScript是如何解析JSX的
- 深入 JSX - React文档
- 区分JSX.Element、ReactNode和ReactElement
背景
setup式组件,期望的写法是这样的:函数组件返回的是一个render方法,而不是React的函数组件返回ReactElement
const Demo = () => {
return () => {
return (<div> this is demo</div>)
}
}
<Demo />这种写法在在Webstorm会提示错误

在VSCode中会提示错误

这个提示是返回值跟ReactElement不匹配。这是因为TypeScript在处理JSX的时候,如果是函数组件,会强制它的返回值可以赋值给JSX.Element。而在@types/react中,定义的JSX.Element为
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
interface ElementClass extends React.Component<any> {
render(): React.ReactNode;
}
}
}这就是导致这个错误提示的原因,我们要解决的也是这个问题。
强制类型转换返回值
一种临时的处理办法是修改ReactElement的定义
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
// type: T;
// props: P;
// key: Key | null;
}虽然问题解决了,但看起来就不太靠谱。
那试试强制类型转换,通过as
function Demo({value = 0}: CountProps) {
const render = () => {
return (<div> this is demo</div>)
}
return render as ReactElement
}会提示一个警告

还要加个unknown
function Demo({value = 0}: CountProps) {
const render = () => {
return (<div> this is demo</div>)
}
return render as unknown as ReactElement
}这个看起来是最简单的方式,但问题在于每个函数组件都要加一行类型转换,对于某些代码洁癖的人来说是不可接受的,因此需要另找出路。
扩展类型定义
我们需要从TypeScript在JSX中的检测机制出发,找到一种比较优雅的解决办法。
扩展React FC定义
我们能不能修改FC的定义,让其返回值支持一个函数呢?
type FC<P = {}> = FunctionComponent<P>;
interface CustomFunctionComponent {
(): ReactElement<any, any> | null
}
interface FunctionComponent<P = {}> {
// 支持返回新的方法
(props: P, context?: any): ReactElement<any, any> | CustomFunctionComponent | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}其中CustomFunctionComponent就是我们定义的新的返回值类型。但遗憾的是,加上之后仍然会报错

很显然我们的CustomFunctionComponent类型无法通过JSX.Element类型的校验。
结论:无法通过修改FC函数组件的签名来通过校验。
ElementClass工厂函数组件
既然TypeScript要校验的是返回值的类型,那只有扩展JSX.Element的类型了。
TypeScript除了支持函数组件,也支持类组件的校验。其中类组件的类型又分为了两种
class类factory工厂函数
// ES6类类型
class MyComponent {
render() {}
}
// 工厂函数类型
function MyFactoryFunction() {
return {
render: () => {
}
}
}除了前者可以写在JSX中之外,后面这种也可以写在JSX中(涨姿势了!!)
JSX标签是对应类的实例类型,元素的实例类型必须赋值给JSX.ElementClass或抛出一个错误,这也是TypeScript要校验的地方
declare namespace JSX {
interface ElementClass {
render: any;
}
}
function MyFactoryFunction() {
return { render: () => {} }
}
function NotAValidFactoryFunction() {
return {};
}
<MyFactoryFunction /> // 正常
<NotAValidFactoryFunction /> // 错误由于在@types/react中重新定义了JSX.Element
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {
}
}
}因此返回的对象还需要type、props和key这三个属性,因此一个工厂函数的组件,其结构大概如下所示
declare namespace JSX {
interface ElementClass {
render(): () => any;
}
}
function MyFactoryFunction(){
return {
key: '',
type: '',
props: {},
render: () => {
return (<div>hello </div>)
}
}
}
<MyFactoryFunction/>; // 正确但是这需要修改组件的编写方式,跟预期的写法有出入,且额外增加了无用的type、props和key这三个字段满足类型检测,只能跟强制类型转换一样,做为备用方案。
结论:可以通过修改工厂函数的返回类型来定义render方法,但需要修改数据结构。
扩展ReactElement定义
既然TypeScript校验的是函数组件的返回值类型是否为ReactElement,我们是不是只要扩展该类型,支持函数就行了?
// 原本的ReactElement定义
interface ReactElementRaw<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
interface CustomFunctionComponent {
(): ReactElement<any, any> | null
}
// 替换原本的ReactElement定义
type ReactElement<P,T> = ReactElementRaw<P,T> | CustomFunctionComponent测试看看,在Webstrom中

在VSCode中

bingo!这就是我想要的效果。
那么现在只剩一个问题,如何在不修改@types/react源码的情况下扩展ReactElement呢?
一种可能是方式是重写namespace,
import {ReactElement as ReactElementRaw} from 'react'
declare global {
interface CustomFunctionComponent {
(): ReactElementRaw<any, any> | null
}
namespace React {
// 重新定义ReactElement,会提示定义重复
type ReactElement = ReactElementRaw | CustomFunctionComponent
}
namespace JSX {
// 同理,在@types/react中也定义了`JSX.Element`,这里也会提示定义重复
type Element = ReactElementRaw | CustomFunctionComponent
}
}但在重定义ReactElement会提示
TS2300: Duplicate identifier 'ReactElement'.
目前TypeScript还不支持强制覆盖类型声明,参考这个issue的讨论。
找了半天,也没有找到合适的方法来实现类型声明覆盖的问题。也许@types/react将JSX.Element类型的定义放开出来也可以,也就是说由开发者自己指定最后的JSX.Element,遗憾的是我也没找到相关的功能。
既然如此,感觉只有参考@types/react,重新写一个声明库了。
重写一个类型库
固有元素
对于HTML标签而言,需要通过JSX.IntrinsicElements来定义。
默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。
如果这个接口存在,那么固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找。
组件
按照上面的约定,我们扩展JSX.Element即可
因此最后的声明文件(极简版)大概是下面这个样子
// src/types/jsx-runtime.d.ts
type Key = string | number;
interface ReactElement<P = any, T = any> {
type: T;
props: P;
key: Key | null;
}
interface CustomFunctionComponent {
(): JSX.Element | null
}
declare global {
namespace JSX {
type Element = ReactElement | CustomFunctionComponent
interface ElementClass {
(prop: any): any
}
interface IntrinsicElements {
[prop: string]: any
}
}
}
export default {}声明文件写好了,怎么在项目中引用呢?配置一下tsconfig.json
// 其他配置省略
{
"compilerOptions": {
"jsx": "react-jsx",
"paths": {
"react/jsx-runtime": [
"./src/types/jsx-runtime.d.ts"
]
}
},
}解释一下,当compilerOptions.jsx配置为react-jsx时,输出的结果为
import { jsx as _jsx } from "react/jsx-runtime";
import React from 'react';
export const HelloWorld = () => _jsx("h1", { children: "Hello world" });详情可参考相关文档。
因此,只需要将react/jsx-runtime这个模块映射到我们自己的模块即可,这就是compilerOptions.paths的作用。
现在,我们有了一个极简版的类型库,并且正常地在项目中运行起来了!

大功告成!
小结
本文主要整理了如何扩展ReactElement类型声明的一些方法,用于支持react-vue中setup返回值类似的写法。
最后通过不太优雅的方式完成了目标:重写一个简易版的类型库,然后通过paths修改react/jsx-runtime达到类型校验的目的。
期间阅读了@types/react相关的源码,并了解了如何实现自定义JSX的一些知识点,还是有点意思。
最重要的是,可以开始使用react-vue来重构博客了哈哈~
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
