import { defineComponent, PropType } from 'vue' // 堆代码 duidaima.com export default defineComponent({ props: { theme: { type: String as PropType<'light' | 'dark'>, required: true, default: 'light', validator: (val: unknown): boolean => { return val === 'light' || val === 'dark' }, }, }, })以上定义 props 方式在庞大的组件库中会存在很大的问题,这段代码中 **type** 定义了可允许的类型,但是它还需要再 **validator** 中再次进行验证才能实现限定值范围的逻辑, **type** 和 **validator**校验逻辑分离,可能会存在人为的错误。如果多个 props 有相同逻辑(默认值,或者限制值范围)需要重复编写。而且开发者需要给每个 props 定义类型,这个会极大降低组件的复用性,因为组件之间的类型很容易匹配不上,导致定位不到类型问题,极难维护。
增强类型推断能力:通过 TypeScript 类型推导,确保 prop 的类型定义与运行时行为一致。
export type EpPropInputDefault< Required extends boolean, Default > = Required extends true ? never : Default extends Record<string, unknown> | Array<any> ? () => Default : (() => Default) | DefaultRequired 泛型希望继承 boolean,下面的推断才有意义。在定义 prop时我们可能会同时定义 type、validator、限定值范围 values和 default的情况,但是如果不给传入的 default 添加一个类型限制,那么开发者可以随意的添加一个不符合 type和 value中声明的类型,这种情况下在编译时就会报错。所以我们需要将默认值同时符合 type、values和 validator的类型。
buildProp({ type: String, values: ['a', 'b'], // 限定值为 'a' 或 'b' default: 'c', // 默认值为 'c' });default 的值 'c' 并不在 'a' | 'b' 范围内。如果没有类型约束,这种错误无法在编译时捕获。在设计合并 type、values和 validator类型时,我们还需要对 **type** 进行一定的处理 ,如果 **type**声明的类型是 String,则它会被推断成 **StringConstructor** 但是我们想要获得 **string** ,我们可以利用 **vue** 的 **ExtractPropType** 来实现,我们还需要将 type 的类型处理成必填和如果 type如果是一个数组类型将它转成可写类型。下面我们来实现这些需求吧。
//将对象类型数组类型转成可写类型 export type Writable<T> = { -readonly [P in keyof T]: T[P] } // 如果类型是数组将其每一项转换为可写类型,如果不是则返回原来的类型。 export type WritableArray<T> = T extends any[] ? Writable<T> : T // 推断类型是否为 never 如果为never 则为 Y 泛型定义类型,否则为N export type IfNever<T, Y = true, N = false> = [T] extends [never] ? Y : N // 推断是否为 unknown 类型,如果是 unknow 则为 Y 泛型定义类型,否则为N export type IfUnknown<T, Y, N> = [unknown] extends [T] ? Y : N // 将 unknown 转成 never 类型,否则原类型返回 export type UnknownToNever<T> = IfUnknown<T, never, T> export default {}在 packages\utils\vue\props\types.ts 文件下封装 buildProp 函数所需所有类型。
/** * Extract all value types from object type T * * 提取对象类型 T 的所有值类型 * @example * Value<{a: string; b: number; c: boolean}> => string | number | boolean * Value< x: { y: number; z: string }; w: boolean> => { y: number; z: string } | boolean */ export type Value<T> = T[keyof T] /** * Extract the type of a single prop * * 提取单个 prop 的参数类型 * @example * ExtractPropType<{ type: StringConstructor; required:true }> => { key: string } => string * ExtractPropType<{ type: StringConstructor }> => { key: string | undefined } => string | undefined * ExtractPropType<{ name: StringConstructor; age: { type: NumberConstructor; required: true }> =>{ key: { name?: string , age: number }} => { name?: string, age:number} */ export type ExtractPropType<T extends object> = Value< ExtractPropTypes<{ key: T }> > /** * Handle the type of prop by converting the type T into a writable and required type. * * 处理 prop 的类型,将 T 类型转换成可写和必填类型 * @example * ResolvePropType<StringConstructor> => string * ResolvePropType<BooleanConstructor> => boolean * ResolvePropType<PropType<T>> => T */ export type ResolvePropType<T> = IfNever< T, never, ExtractPropType<{ type: WritableArray<T>; required: true }> > /** * The final type of prop after merging the type, required, default value, and validator. * * 合并类型、是否必填、默认值和验证器后的最终 prop 类型 * 优先使用 values 的类型(Value)。如果没有 values,则使用 type 推断类型。如果 Value 是有效类型,则直接使用 Value 的类型,如果Validator 有类型则使用 validator 推断类型 * @example * EpPropMergeType<StringConstructor, 'a' | 'b', number> => "a" | "b" | number */ export type EpPropMergeType<Type, Value, Validator> = | IfNever<UnknownToNever<Value>, ResolvePropType<Type>, never> | UnknownToNever<Value> | UnknownToNever<Validator>以上我们就已经封装好合并处理 Type 、Value 、Validator 类型 EpPropMergeType。这个类型很完美了处理了各种输入类型,默认值类型default也应该符合这个合并类型。处理好这个类型我们就可以继续配置 prop 的输入类型了。
export type EpPropInput< Type, Value, Validator, Default extends EpPropMergeType<Type, Value, Validator>, Required extends boolean > = { type?: Type required?: Required values?: readonly Value[] validator?: ((val: any) => val is Validator) | ((val: any) => boolean) default?: EpPropInputDefault<Required, Default> }2).输出类型 EpPropFinalized 的设计
/** * output prop `buildProp` or `buildProps`. * * prop 输出参数。 * * @example * EpProp<'a', 'b', true> * ⬇️ * { readonly type: PropType<"a">; readonly required: true; readonly validator: ((val: unknown) => boolean) | undefined; readonly default: "b"; __epPropKey: true; } */ export type EpProp<Type, Default, Required> = { readonly type: PropType<Type> readonly required: [Required] extends [true] ? true : false readonly validator: ((val: unknown) => boolean) | undefined [epPropKey]: true } & IfNever<Default, unknown, { readonly default: Default }>但是这个输出类型中的 Type 泛型也需要做出一定的优化,我们希望输出后的 Type 应该是 Value、Type和 Validator 合并好的类型得出的最后的结果类型。
/** * Finalized conversion output * * 最终转换 EpProp */ export type EpPropFinalized<Type, Value, Validator, Default, Required> = EpProp< EpPropMergeType<Type, Value, Validator>, UnknownToNever<Default>, Required >这个 unknownToNever主要是为了解决,如果 Default 传入的泛型是 unknown 但是 default 又有具体的值,这时就会和这个泛型设定的值引发歧义。所以我们需要将 unknown 类型转换成 never 再交给 EpProp 来对 Default类型再做处理。
export const isEpProp = (val: unknown): val is EpProp<any, any, any> => isObject(val) && !!(val as any)[epPropKey] export const buildProp = < Type = never, Value = never, Validator = never, Default extends EpPropMergeType<Type, Value, Validator> = never, Required extends boolean = false >( prop: EpPropInput<Type, Value, Validator, Default, Required>, key?: string ): EpPropFinalized<Type, Value, Validator, Default, Required> => { // filter native prop type and nested prop, e.g `null`, `undefined` (from `buildProps`) if (!isObject(prop) || isEpProp(prop)) return prop as any const { values, required, default: defaultValue, type, validator } = prop const _validator = values || validator ? (val: unknown) => { let valid = false let allowedValues: unknown[] = [] if (values) { allowedValues = Array.from(values) if (hasOwn(prop, 'default')) { allowedValues.push(defaultValue) } valid ||= allowedValues.includes(val) } if (validator) valid ||= validator(val) if (!valid && allowedValues.length > 0) { const allowValuesText = [...new Set(allowedValues)] .map((value) => JSON.stringify(value)) .join(', ') warn( `Invalid prop: validation failed${ key ? ` for prop "${key}"` : '' }. Expected one of [${allowValuesText}], got value ${JSON.stringify( val )}.` ) } return valid } : undefined const epProp: any = { type, required: !!required, validator: _validator, [epPropKey]: true, } if (hasOwn(prop, 'default')) epProp.default = defaultValue return epProp }三、buildProps 函数的具体实现
/** * Native prop types, e.g: `BooleanConstructor`, `StringConstructor`, `null`, `undefined`, etc. * * 原生 prop `类型,BooleanConstructor`、`StringConstructor`、`null`、`undefined` 等 */ export type NativePropType = | ((...args: any) => any) | { new (...args: any): any } | undefined | null export type IfNativePropType<T, Y, N> = [T] extends [NativePropType] ? Y : N我们对输入类型做了限制,我们这个函数的输出类型也需要有一定的范围,这个函数单个 prop返回值类型要不就是 EpProp类型,要不就是 NativePropType ,如果这个类型不是这个原生类型也不是 EpProp类型,那么我们需要将它转换成我们想要的类型。所以我们也需要重新动态声明转换类型。
export type EpPropConvert<Input> = Input extends EpPropInput< infer Type, infer Value, infer Validator, any, infer Required > ? EpPropFinalized<Type, Value, Validator, Input['default'], Required> : never如果 Input 类型符合 EpPropInput 类型的话则它会通过 infer 提取这些 Type、Value... 类型并以 EpPropFinalized 类型输出,否则直接为 never。这样做的目的是可以严格规范这个函数输入的类型符合 buildProp输出的类型。
export const buildProps = < Props extends Record< string, | { [epPropKey]: true } | NativePropType | EpPropInput<any, any, any, any, any> > >( props: Props ): { [K in keyof Props]: IfEpProp< Props[K], Props[K], IfNativePropType<Props[K], Props[K], EpPropConvert<Props[K]>> > } => fromPairs( Object.entries(props).map(([key, option]) => [ key, buildProp(option as any, key), ]) ) as any使用 Object.entries 将 props 转换为 [key, value] 的数组。 将 option传入 buildProp函数处理,传入 key,用于输出更详细的警告信息。 使用 fromPairs 将处理后的 [key, processedProp] 数组重新转换为对象,将数组对象转换成对象的形式输出。