闽公网安备 35020302035485号
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] 数组重新转换为对象,将数组对象转换成对象的形式输出。