TypeScript如何处理JSX
TypeScript参考了React的JSX,那么我们就以React为例,来分析TypeScript是如何处理React的JSX的。
<div id="main">
<MyComponent title="my-component"></MyComponent>
</div>
这段代码足以囊括React的JSX使用的各种方面:宿主元素、宿主元素属性、自定义组件、自定义组件属性、Children,其实看似复杂的JSX片段无非就这几个概念。TypeScript为了能准确识别出这些概念,定义了若干个接口,并要求用户自主选择这些接口的实现形式,从而让TypeScript建立起与React JSX之间的映射关系。
TypeScript定义的若干接口如下:
React JSX | TypeScript JSX 定义 |
---|---|
宿主元素 | IntrinsicElements |
宿主元素属性 | IntrinsicAttributes IntrinsicClassAttributes |
自定义组件 | Element ElementClass 关于这两者的区别:www.typescriptlang.org/docs/handbo… |
自定义组件属性 | 函数组件:函数的第一个参数 Class组件:ElementAttributesProperty |
Children | ElementChildrenAttribute |
TypeScript如何处理React的JSX
我们以React提供的TypeScript JSX定义为例来分析:
declare global {
namespace JSX {
// tslint:disable-next-line:no-empty-interface
interface Element extends React.ReactElement<any, any> { }
interface ElementClass extends React.Component<any> {
render(): React.ReactNode;
}
interface ElementAttributesProperty { props: {}; }
interface ElementChildrenAttribute { children: {}; }
// tslint:disable-next-line:no-empty-interface
interface IntrinsicAttributes extends React.Attributes { }
// tslint:disable-next-line:no-empty-interface
interface IntrinsicClassAttributes<T> extends React.ClassAttributes<T> { }
interface IntrinsicElements {
// HTML
...
form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>;
h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>;
...
}
}
}
使用这个JSX配置,可以告诉TypeScript:
遇到了Element,去使用React.ReactElement做类型检查
对于Class组件的实例,即this,去使用React.Component做类型检查
遇到了Class类型的Element,它上面的属性,去使用ElementClass的props属性检查
遇到了函数类型的Element,它上面的属性,直接使用函数的第一个参数检查
注意,这条规则是TypeScript默认的,并不是上面的配置文件得来的
遇到Element,它的Children,去使用ElementClass的props的children属性检查
遇到IntrinsicElement,直接使用IntrinsicElements的定义检查,即这段代码:
{ // HTML ... form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>; h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>; ... }
遇到IntrinsicElement,它的普通属性和class属性,分别使用React.Attributes和React.ClassAttributes检查
TypeScript如何知道是Element(自定义组件)还是IntrinsicElement(宿主组件)的?
通过"<"之后的首字母的大小写来判断,如果是大写就是Element,如果是小写就是IntrinsicElement。
TypeScript如何处理Vue的JSX
再来看看我们在vue项目中常用的TypeScript的JSX配置:
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
使用这个JSX配置,可以告诉TypeScript:
遇到了Element,去使用Vue的VNode做类型检查
对于Class组件的实例,即this,使用Vue做类型检查
遇到了Class类型的Element,它上面的属性,未定义
遇到了函数类型的Element,它上面的属性,直接使用函数的第一个参数检查
注意,这条规则是TypeScript默认的,并不是上面的配置文件得来的
遇到Element,它的Children,未定义
遇到IntrinsicElement,直接使用IntrinsicElements的定义检查
在这里虽然的确定义了IntrinsicElements,但是这种写法相当于:允许任何宿主标签,即便HTML中压根没有,比如<meituan></meituan>,定义不完全。
遇到IntrinsicElement,它的普通属性和class属性,未定义
通过对比可以发现,我们在Vue中常用的TypeScript的JSX定义远不如React详细,我们通过这个配置甚至可以直接给出以下结论:
Vue中的JSX的Class类型的组件,不具备props的校验能力
Vue中的JSX的Children,不具备校验能力
Vue中的JSX的宿主组件和宿主组件的属性,都不具备校验能力
这与我们的实践结果是完全一致的。
如何让TypeScript完美兼容Vue2.x
我们只需要把上述未定义或定义不完全的部分补上就可以了,我们先简单的思考一下:
遇到了Class类型的Element,它上面的属性,想当然的使用Vue中的$props属性
遇到Element,它的Children,想当然的使用Vue的$slots属性
IntrinsicElement补充完整
对于第3点是可行的,只是一些工作量而已。但是对于第1点和第2点,就没有那么简单了。
对于第1点:Vue的$props压根就没有泛型的概念,就算你指定了$props也没用,TypeScript依然是摆设,如图:
readonly $props: Record<string, any>;
对于第2点:Vue的$slots不仅没有泛型概念,还跟TypeScript的children定义存在冲突。TypeScript定义的children应该是一个Element或者Element数组,但是在Vue中$slots却是一个key:value的对象。
readonly $slots: { [key: string]: VNode[] | undefined };
readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
所以,我们没法简单的补充Vue的JSX定义。我们需要给予Vue的props泛型的能力,我们还需要抹平Vue的slots和TypeScript的children之间定义的差异。我们必须做一些HACK。
vue-tsx-support的解决方案
通过引入vue-tsx-support提供的JSX定义文件,便可以实现我们的诉求,它的定义内容如下:
import * as base from "./types/base";
import * as builtin from "./types/builtin-components";
import "./types/vue";
declare global {
namespace JSX {
interface Element extends base.Element {}
interface ElementClass extends base.ElementClass {}
interface ElementAttributesProperty extends base.ElementAttributesProperty {}
interface IntrinsicElements extends base.IntrinsicElements {
// allow unknown elements
[name: string]: any;
// builtin components
transition: base.TsxComponentAttrs<builtin.TransitionProps>;
"transition-group": base.TsxComponentAttrs<builtin.TransitionGroupProps>;
"keep-alive": base.TsxComponentAttrs<builtin.KeepAliveProps>;
}
}
}
可以看到,vue-tsx-supoort把关键的Element、ElementClass、ElementAttributesProperty和IntrinsicElements这四个定义,使用自己定义的JSX类型做了替换。
通过查看代码,我们发现base.Element和bae.ElementClass其实还是VNode和Vue,IntrinsicElements也比较好理解,所以接下来我们主要分析ElementAttributesProperty。
ElementAttributesProperty的定义如下:
export interface ElementAttributesProperty {
_tsxattrs: any;
}
也就是说,vue-tsx-support告诉TypeScript在遇到Class组件的属性时,应该去ElementClass的_tsxattrs去做类型检查。那么这个_tsxattrs属性又是哪里来的呢?
通过vue-tsx-support官方给出的使用例子可以看到:
import * as tsx from "vue-tsx-support";
const MyComponent = tsx.componentFactory.create({
props: {
text: { type: String, required: true },
important: Boolean,
},
...
});
我们必须使用vue-tsx-support的方法来创建Vue实例,所以vue-tsx-support必定会对我们的Vue实例做一些"手脚"。一路的寻找,我们最终会发现这段代码:
export class Component<
Props,
EventsWithOn = {},
ScopedSlotArgs = {}
> extends Vue {
_tsxattrs!: TsxComponentAttrs<Props, EventsWithOn, ScopedSlotArgs>;
$scopedSlots!: {
[K in keyof ScopedSlotArgs]: InnerScopedSlot<ScopedSlotArgs[K]>
};
}
也就是说,vue-tsx-supoort会使用自己定义的Component去扩展我们的Vue实例,并且在上面添加了_tsxattrs属性,修改了$scopedSlots属性。更重要的是,在这里我们看到Component是支持泛型的,那么这就给了我们在书写Vue组件时,指定props和scopedSlots类型的能力。
最后,为了OO开发,我们会结合使用vue-class-component和vue-tsx-supoort:
import component from "vue-class-component";
import * as tsx from "vue-tsx-support";
interface MyComponentProps {
text: string;
important?: boolean;
}
@component({
props: {
text: { type: String, required: true },
important: Boolean
},
/* snip */
})
class MyComponent extends tsx.Component<MyComponentProps> {
/* snip */
}
唯一遗憾的是,我们必须把props声明两次,一次给vue-class-component,一次给vue-tsx-component。
到这里,我们基本解决了Vue的JSX中Class组件属性的类型校验。
依然没有解决的问题——slots
vue-tsx-support虽然给予了Vue的props泛型的能力,让TypeScript得意分析Class组件的属性。但是困扰我们的另一个问题依然没有解决,那就是slots与children之间的定义冲突问题。也就是说,即便用了vue-tsx-supoort,TypeScript依然无法对Element的Children进行检查。
scopedSlots其实就是React的renderProps,它本质上还是props,跟slots没关系。
题外话
可能我们会有这样的疑问:Vue3都要出来了,还在这儿费劲折腾Vue2的TypeScript有什么意义呢?
经常看到有人说:Vue3是用TypeScript写的,所以TypeScript的支持肯定没问题。但是通过本文和《为什么使用TypeScript开发Vue2.x体验很差》的分析可以得知,这句话其实是不严谨的。
所以,在这里折腾还有意义的,不仅加深了我们对TypeScript的理解,而且还有助于我们独立思考。
参考
完。