问题背景
在使用 Vue + TypeScript 的时候,应该都会有个疑问,这些选项都是怎么推导出来的?特别是 props
这个选项,有着好几种形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| props: ['hello']
props: { hello: String, }
props: { hello: [String, Boolean], }
props: { hello: { type: String, default: 'hello', }, }
|
有着这么多种形式,最后还能在其他地方推导出正确的类型
怎么做到的?
查看源码
我们从 Vue.extend
点进去,找到 vue.d.ts
中定义的 extend
:
1 2 3 4 5
| export interface VueConstructor<V extends Vue = Vue> { extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; }
|
顺着 Props 相关的类型,找到 vue 的 options.d.ts
文件中的 ComponentOptions
接口,这是我们使用 Vue.extend
时传入选项的接口。
可以看到 props?: PropsDef;
我们顺着这个 PropsDef
把 Props 相关的定义都提取出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export type Prop<T> = { (): T } | { new(...args: any[]): T & object } | { new(...args: string[]): Function }
export type PropType<T> = Prop<T> | Prop<T>[];
export type PropValidator<T> = PropOptions<T> | PropType<T>;
export interface PropOptions<T=any> { type?: PropType<T>; required?: boolean; default?: T | null | undefined | (() => T | null | undefined); validator?(value: T): boolean; }
export type RecordPropsDefinition<T> = { [K in keyof T]: PropValidator<T[K]> } export type ArrayPropsDefinition<T> = (keyof T)[]; export type PropsDefinition<T> = ArrayPropsDefinition<T> | RecordPropsDefinition<T>;
export interface ComponentOptions< // ... PropsDef=PropsDefinition<DefaultProps>, Props=DefaultProps> { props?: PropsDef; }
|
简化代码
上边源码看起来还是有点多,我这边大概理解了推导过程,所以写一个简化版的便于理解,只考虑 props 为对象的情况:
1 2 3 4 5
| Vue.extend({ props: { test: String, } })
|
简化的类型代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type Prop<T> = () => T
type PropDef<T> = { [K in keyof T]: Prop<T[K]> }
interface PropsConstructor { extend<Prop>(options?: PropDef<Prop>): Prop }
declare const Props: PropsConstructor
const props = Props.extend({ test: String, })
|
把上述简化的代码贴到 VSCode ,把鼠标移到 const props
上,可以发现正确推导出了 { test: string }
。
怎么做到的呢?这里我之前犯了个错误,把 Prop
这个类型当成是我们传进去的对象了,所以一直没法理解。
而实际上,PropDef<Prop>
才是我们传进去的类型,这边是 通过 PropDef<Prop>
推导出了 Prop
我们把 { test: string }
从上往下代入类型中,就是:
1 2 3 4 5 6 7 8 9 10 11 12 13
| type Prop<string> = () => string
type PropDef<{ test: string, }> = { test: Prop<string>; }
interface PropsConstructor { extend<{ test: string }>(options?: PropDef<{ test: string }>): { test: string } }
|
代入后,不知各位会不会清楚一些。
这边的重点就是 通过 PropDef<Prop>
推导出了 Prop
我们写 Vue.extend
的时候定义的是 PropDef<Prop>
,而 Prop
则是通过 TypeScript 推导出来的,因此我们在其他地方才能获得正确的类型。
其他情况
如果理解了上面那种情况,那其他情况其实就比较好理解了。
我们看到 vue 里面有这样一条类型定义:
1
| export type PropsDefinition<T> = ArrayPropsDefinition<T> | RecordPropsDefinition<T>;
|
显然,这是区分了 props 是数组跟对象的情况。
数组
首先看数组:
1
| export type ArrayPropsDefinition<T> = (keyof T)[];
|
数组的情况比较简单,假设我们传入的是 ['test']
,那么就直接对应了这个类型,在 extend
时会转为一个对象,类型都是 any
,即 { test: any }
。
对象
然后是对象:
1 2 3 4 5 6 7
| export type PropType<T> = Prop<T> | Prop<T>[];
export type PropValidator<T> = PropOptions<T> | PropType<T>;
export type RecordPropsDefinition<T> = { [K in keyof T]: PropValidator<T[K]> }
|
这边的 PropType<T>
基本对应了刚刚简化版的情况,不过还加了一个数组的情况,就是:
1 2 3 4 5 6 7 8 9 10 11
| Vue.extend({ props: { test: String, }, })
Vue.extend({ props: { test: [String, Boolean], }, })
|
对应这两种情况。
最后的 PropOptions<T>
就是对应最复杂的那种情况了:
1 2 3 4 5 6 7 8
| Vue.extend({ props: { test: { type: String, default: 'xxx', }, }, })
|
这个结构对应 PropOptions<T>
:
1 2 3 4 5 6
| export interface PropOptions<T=any> { type?: PropType<T>; required?: boolean; default?: T | null | undefined | (() => T | null | undefined); validator?(value: T): boolean; }
|
PropOptions
的那个 T
就是我们最后推导拿到的类型,即 { test: string }
多个 extend 方法的重载
在 vue.d.ts
中,可以看到 VueConstructor
有很多 extend
方法的重载:
1 2 3 4 5 6 7 8 9 10 11
| export interface VueConstructor<V extends Vue = Vue> {
extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>; extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>; extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
}
|
这边通过 options
参数的类型,可以看出应对不同 props
形式而做的重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export interface VueConstructor<V extends Vue = Vue> {
extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
}
|
结论
通过 PropDef<Prop>
推导出了 Prop
就是本次记录的结论,你在 VueConstructor['extend']
的多种重载中看到的 Props
,都是已经推导好的类型,而不是用户传进去的代码。
学过 TypeScript 的各位大佬们可能很快就理解了这种用法,我比较迟钝,记录一下hhh。