mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
refactor(utils): ep prop (#8018)
This commit is contained in:
parent
6359538a45
commit
362c7f6b3c
@ -2,6 +2,7 @@ import { computed, getCurrentInstance, onMounted, watch } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { isClient } from '@vueuse/core'
|
||||
import { buildProp, definePropType, isBoolean } from '@element-plus/utils'
|
||||
import type { ExtractPropType } from '@element-plus/utils'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, Ref } from 'vue'
|
||||
@ -14,19 +15,27 @@ const _event = buildProp({
|
||||
type: definePropType<(val: boolean) => void>(Function),
|
||||
} as const)
|
||||
|
||||
type _UseModelToggleProps<T extends string> = {
|
||||
export type UseModelTogglePropsRaw<T extends string> = {
|
||||
[K in T]: typeof _prop
|
||||
} & {
|
||||
[K in `onUpdate:${T}`]: typeof _event
|
||||
}
|
||||
|
||||
export type UseModelTogglePropsGeneric<T extends string> = {
|
||||
[K in T]: ExtractPropType<typeof _prop>
|
||||
} & {
|
||||
[K in `onUpdate:${T}`]: ExtractPropType<typeof _event>
|
||||
}
|
||||
|
||||
export const createModelToggleComposable = <T extends string>(name: T) => {
|
||||
const updateEventKey = `update:${name}` as const
|
||||
const updateEventKeyRaw = `onUpdate:${name}` as const
|
||||
const useModelToggleEmits = [updateEventKey]
|
||||
|
||||
const useModelToggleProps = {
|
||||
[name]: _prop,
|
||||
[`onUpdate:${name}`]: _event,
|
||||
} as _UseModelToggleProps<T>
|
||||
|
||||
const useModelToggleEmits = [`update:${name}`]
|
||||
[updateEventKeyRaw]: _event,
|
||||
} as UseModelTogglePropsRaw<T>
|
||||
|
||||
const useModelToggle = ({
|
||||
indicator,
|
||||
@ -37,15 +46,12 @@ export const createModelToggleComposable = <T extends string>(name: T) => {
|
||||
onHide,
|
||||
}: ModelToggleParams) => {
|
||||
const instance = getCurrentInstance()!
|
||||
const props = instance.props as _UseModelToggleProps<T> & {
|
||||
const { emit } = instance
|
||||
const props = instance.props as UseModelTogglePropsGeneric<T> & {
|
||||
disabled: boolean
|
||||
}
|
||||
const { emit } = instance
|
||||
|
||||
const updateEventKey = `update:${name}`
|
||||
|
||||
const hasUpdateHandler = computed(() =>
|
||||
isFunction(props[`onUpdate:${name}`])
|
||||
isFunction(props[updateEventKeyRaw])
|
||||
)
|
||||
// when it matches the default value we say this is absent
|
||||
// though this could be mistakenly passed from the user but we need to rule out that
|
||||
@ -135,7 +141,7 @@ export const createModelToggleComposable = <T extends string>(name: T) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props[name], onChange as any)
|
||||
watch(() => props[name], onChange)
|
||||
|
||||
if (
|
||||
shouldHideWhenRouteChanges &&
|
||||
@ -158,7 +164,7 @@ export const createModelToggleComposable = <T extends string>(name: T) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onChange(props[name] as boolean)
|
||||
onChange(props[name])
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -5,22 +5,110 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { expectTypeOf } from 'expect-type'
|
||||
import { buildProp, buildProps, definePropType, keysOf, mutable } from '../..'
|
||||
import type { propKey } from '../..'
|
||||
import type {
|
||||
EpProp,
|
||||
EpPropInputDefault,
|
||||
EpPropMergeType,
|
||||
IfNever,
|
||||
ResolvePropType,
|
||||
UnknownToNever,
|
||||
Writable,
|
||||
WritableArray,
|
||||
epPropKey,
|
||||
} from '../..'
|
||||
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
|
||||
describe('Types', () => {
|
||||
it('Writable', () => {
|
||||
expectTypeOf<Writable<readonly [1, 2, 3]>>().toEqualTypeOf<[1, 2, 3]>()
|
||||
expectTypeOf<Writable<Readonly<{ a: 'b' }>>>().toEqualTypeOf<{
|
||||
a: 'b'
|
||||
}>()
|
||||
expectTypeOf<Writable<123>>().toEqualTypeOf<123>()
|
||||
expectTypeOf<
|
||||
Writable<StringConstructor>
|
||||
>().not.toEqualTypeOf<StringConstructor>()
|
||||
})
|
||||
|
||||
it('WritableArray', () => {
|
||||
expectTypeOf<WritableArray<readonly [1, 2, 3]>>().toEqualTypeOf<[1, 2, 3]>()
|
||||
expectTypeOf<
|
||||
WritableArray<BooleanConstructor>
|
||||
>().toEqualTypeOf<BooleanConstructor>()
|
||||
})
|
||||
|
||||
it('IfNever', () => {
|
||||
expectTypeOf<IfNever<boolean | 123 | '1'>>().toEqualTypeOf<false>()
|
||||
expectTypeOf<IfNever<never>>().toEqualTypeOf<true>()
|
||||
})
|
||||
|
||||
it('UnknownToNever', () => {
|
||||
expectTypeOf<UnknownToNever<unknown>>().toEqualTypeOf<never>()
|
||||
expectTypeOf<UnknownToNever<unknown | 1>>().toEqualTypeOf<never>()
|
||||
expectTypeOf<UnknownToNever<1>>().toEqualTypeOf<1>()
|
||||
})
|
||||
|
||||
it('ResolvePropType', () => {
|
||||
expectTypeOf<ResolvePropType<BooleanConstructor>>().toEqualTypeOf<boolean>()
|
||||
expectTypeOf<ResolvePropType<StringConstructor>>().toEqualTypeOf<string>()
|
||||
expectTypeOf<ResolvePropType<DateConstructor>>().toEqualTypeOf<Date>()
|
||||
expectTypeOf<
|
||||
ResolvePropType<[DateConstructor, NumberConstructor]>
|
||||
>().toEqualTypeOf<Date | number>()
|
||||
expectTypeOf<
|
||||
ResolvePropType<readonly [DateConstructor, NumberConstructor]>
|
||||
>().toEqualTypeOf<Date | number>()
|
||||
expectTypeOf<
|
||||
ResolvePropType<PropType<string | 12 | false>>
|
||||
>().toEqualTypeOf<string | 12 | false>()
|
||||
expectTypeOf<ResolvePropType<never>>().toBeNever()
|
||||
})
|
||||
|
||||
it('EpPropMergeType', () => {
|
||||
expectTypeOf<
|
||||
EpPropMergeType<StringConstructor | NumberConstructor, 'str', 1>
|
||||
>().toEqualTypeOf<'str' | 1>()
|
||||
expectTypeOf<EpPropMergeType<NumberConstructor, 2 | 3, 4>>().toEqualTypeOf<
|
||||
2 | 3 | 4
|
||||
>()
|
||||
})
|
||||
|
||||
it('EpPropInputDefault', () => {
|
||||
expectTypeOf<EpPropInputDefault<true, 1>>().toBeNever()
|
||||
expectTypeOf<EpPropInputDefault<false, 1>>().toEqualTypeOf<1 | (() => 1)>()
|
||||
})
|
||||
|
||||
it('EpProp', () => {
|
||||
expectTypeOf<EpProp<'1', '2', false>>().toEqualTypeOf<{
|
||||
readonly type: PropType<'1'>
|
||||
readonly required: false
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
readonly default: '2'
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf<EpProp<'1', '2', true>>().toEqualTypeOf<{
|
||||
readonly type: PropType<'1'>
|
||||
readonly required: true
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
readonly default: '2'
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildProp', () => {
|
||||
it('Only type', () => {
|
||||
expectTypeOf(
|
||||
buildProp({
|
||||
type: definePropType<'a' | 'b'>(String),
|
||||
})
|
||||
} as const)
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<'a' | 'b'>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -32,24 +120,22 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<1 | 2 | 3 | 4>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
it('Type and values', () => {
|
||||
expectTypeOf(
|
||||
buildProp({
|
||||
type: definePropType<number[]>(Array),
|
||||
type: Number,
|
||||
values: [1, 2, 3, 4],
|
||||
} as const)
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<1 | 2 | 3 | 4 | number[]>
|
||||
readonly type: PropType<1 | 2 | 3 | 4>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -62,9 +148,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<number | 'a' | 'b' | 'c'>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -77,9 +162,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<'a' | 'b' | 'c'>
|
||||
readonly required: true
|
||||
readonly default?: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -95,7 +179,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: 'b'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -110,7 +194,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: ['a', 'b']
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -129,7 +213,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: { key: 'value' }
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -148,7 +232,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: { key: string }
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -162,9 +246,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<number | 'a' | 'b' | 'c'>
|
||||
readonly required: true
|
||||
readonly default?: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -176,9 +259,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<string>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -186,9 +268,8 @@ describe('buildProp', () => {
|
||||
expectTypeOf(buildProp({ type: [String, Number, Boolean] })).toEqualTypeOf<{
|
||||
readonly type: PropType<string | number | boolean>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -201,9 +282,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<'1' | '2' | '3'>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -216,9 +296,8 @@ describe('buildProp', () => {
|
||||
).toEqualTypeOf<{
|
||||
readonly type: PropType<string>
|
||||
readonly required: true
|
||||
readonly default?: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -233,7 +312,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: 'a'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -248,7 +327,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: { key: 'a' }
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -263,7 +342,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: ''
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -278,7 +357,7 @@ describe('buildProp', () => {
|
||||
readonly required: false
|
||||
readonly default: {}
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
|
||||
@ -363,23 +442,21 @@ describe('buildProps', () => {
|
||||
readonly required: false
|
||||
readonly default: 'hello'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key1).toEqualTypeOf<{
|
||||
readonly type: PropType<'a' | 'b'>
|
||||
readonly required: false
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
readonly default: undefined
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key2).toEqualTypeOf<{
|
||||
readonly type: PropType<1 | 2 | 3 | 4>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key3).toEqualTypeOf<{
|
||||
@ -387,7 +464,7 @@ describe('buildProps', () => {
|
||||
readonly required: false
|
||||
readonly default: 2
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key4).toEqualTypeOf<{
|
||||
@ -395,7 +472,7 @@ describe('buildProps', () => {
|
||||
readonly required: false
|
||||
readonly default: 'a'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key5).toEqualTypeOf<BooleanConstructor>()
|
||||
@ -409,17 +486,17 @@ describe('buildProps', () => {
|
||||
expectTypeOf(props.key12).toEqualTypeOf<{
|
||||
readonly type: PropType<string>
|
||||
readonly required: false
|
||||
readonly default: undefined
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key13).toEqualTypeOf<{
|
||||
readonly type: PropType<string | number | Function>
|
||||
readonly required: false
|
||||
readonly default: '123'
|
||||
// TODO
|
||||
readonly default: () => '123'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key14).toEqualTypeOf<{
|
||||
@ -427,7 +504,7 @@ describe('buildProps', () => {
|
||||
readonly required: false
|
||||
readonly default: () => '123'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key15).toEqualTypeOf<{
|
||||
@ -435,15 +512,16 @@ describe('buildProps', () => {
|
||||
readonly required: false
|
||||
readonly default: () => () => '123'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
|
||||
expectTypeOf(props.key16).toEqualTypeOf<{
|
||||
readonly type: PropType<string>
|
||||
readonly required: false
|
||||
readonly default: '123'
|
||||
// TODO
|
||||
readonly default: () => '123'
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
[epPropKey]: true
|
||||
}>()
|
||||
})
|
||||
})
|
||||
|
@ -1,207 +0,0 @@
|
||||
import { warn } from 'vue'
|
||||
import { fromPairs } from 'lodash-unified'
|
||||
import { isObject } from '../types'
|
||||
import { hasOwn } from '../objects'
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
|
||||
const wrapperKey = Symbol()
|
||||
export type PropWrapper<T> = { [wrapperKey]: T }
|
||||
|
||||
export const propKey = '__elPropsReservedKey'
|
||||
|
||||
type ResolveProp<T> = ExtractPropTypes<{
|
||||
key: { type: T; required: true }
|
||||
}>['key']
|
||||
type ResolvePropType<T> = ResolveProp<T> extends { type: infer V }
|
||||
? V
|
||||
: ResolveProp<T>
|
||||
type ResolvePropTypeWithReadonly<T> = Readonly<T> extends Readonly<
|
||||
Array<infer A>
|
||||
>
|
||||
? ResolvePropType<A[]>
|
||||
: ResolvePropType<T>
|
||||
|
||||
type IfUnknown<T, V> = [unknown] extends [T] ? V : T
|
||||
|
||||
export type BuildPropOption<T, D extends BuildPropType<T, V, C>, R, V, C> = {
|
||||
type?: T
|
||||
values?: readonly V[]
|
||||
required?: R
|
||||
default?: R extends true
|
||||
? never
|
||||
: D extends Record<string, unknown> | Array<any>
|
||||
? () => D
|
||||
: (() => D) | D
|
||||
validator?: ((val: any) => val is C) | ((val: any) => boolean)
|
||||
}
|
||||
|
||||
type _BuildPropType<T, V, C> =
|
||||
| (T extends PropWrapper<unknown>
|
||||
? T[typeof wrapperKey]
|
||||
: [V] extends [never]
|
||||
? ResolvePropTypeWithReadonly<T>
|
||||
: never)
|
||||
| V
|
||||
| C
|
||||
export type BuildPropType<T, V, C> = _BuildPropType<
|
||||
IfUnknown<T, never>,
|
||||
IfUnknown<V, never>,
|
||||
IfUnknown<C, never>
|
||||
>
|
||||
|
||||
type _BuildPropDefault<T, D> = [T] extends [
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
Record<string, unknown> | Array<any> | Function
|
||||
]
|
||||
? D
|
||||
: D extends () => T
|
||||
? ReturnType<D>
|
||||
: D
|
||||
|
||||
export type BuildPropDefault<T, D, R> = R extends true
|
||||
? { readonly default?: undefined }
|
||||
: {
|
||||
readonly default: Exclude<D, undefined> extends never
|
||||
? undefined
|
||||
: Exclude<_BuildPropDefault<T, D>, undefined>
|
||||
}
|
||||
export type BuildPropReturn<T, D, R, V, C> = {
|
||||
readonly type: PropType<BuildPropType<T, V, C>>
|
||||
readonly required: IfUnknown<R, false>
|
||||
readonly validator: ((val: unknown) => boolean) | undefined
|
||||
[propKey]: true
|
||||
} & BuildPropDefault<
|
||||
BuildPropType<T, V, C>,
|
||||
IfUnknown<D, never>,
|
||||
IfUnknown<R, false>
|
||||
>
|
||||
|
||||
/**
|
||||
* @description Build prop. It can better optimize prop types
|
||||
* @description 生成 prop,能更好地优化类型
|
||||
* @example
|
||||
// limited options
|
||||
// the type will be PropType<'light' | 'dark'>
|
||||
buildProp({
|
||||
type: String,
|
||||
values: ['light', 'dark'],
|
||||
} as const)
|
||||
* @example
|
||||
// limited options and other types
|
||||
// the type will be PropType<'small' | 'large' | number>
|
||||
buildProp({
|
||||
type: [String, Number],
|
||||
values: ['small', 'large'],
|
||||
validator: (val: unknown): val is number => typeof val === 'number',
|
||||
} as const)
|
||||
@link see more: https://github.com/element-plus/element-plus/pull/3341
|
||||
*/
|
||||
export function buildProp<
|
||||
T = never,
|
||||
D extends BuildPropType<T, V, C> = never,
|
||||
R extends boolean = false,
|
||||
V = never,
|
||||
C = never
|
||||
>(
|
||||
option: BuildPropOption<T, D, R, V, C>,
|
||||
key?: string
|
||||
): BuildPropReturn<T, D, R, V, C> {
|
||||
// filter native prop type and nested prop, e.g `null`, `undefined` (from `buildProps`)
|
||||
if (!isObject(option) || !!option[propKey]) return option as any
|
||||
|
||||
const { values, required, default: defaultValue, type, validator } = option
|
||||
|
||||
const _validator =
|
||||
values || validator
|
||||
? (val: unknown) => {
|
||||
let valid = false
|
||||
let allowedValues: unknown[] = []
|
||||
|
||||
if (values) {
|
||||
allowedValues = Array.from(values)
|
||||
if (hasOwn(option, '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 prop: any = {
|
||||
type:
|
||||
isObject(type) && Object.getOwnPropertySymbols(type).includes(wrapperKey)
|
||||
? type[wrapperKey]
|
||||
: type,
|
||||
required: !!required,
|
||||
validator: _validator,
|
||||
[propKey]: true,
|
||||
}
|
||||
if (hasOwn(option, 'default')) prop.default = defaultValue
|
||||
|
||||
return prop as BuildPropReturn<T, D, R, V, C>
|
||||
}
|
||||
|
||||
type NativePropType = [
|
||||
((...args: any) => any) | { new (...args: any): any } | undefined | null
|
||||
]
|
||||
|
||||
export const buildProps = <
|
||||
O extends {
|
||||
[K in keyof O]: O[K] extends BuildPropReturn<any, any, any, any, any>
|
||||
? O[K]
|
||||
: [O[K]] extends NativePropType
|
||||
? O[K]
|
||||
: O[K] extends BuildPropOption<
|
||||
infer T,
|
||||
infer D,
|
||||
infer R,
|
||||
infer V,
|
||||
infer C
|
||||
>
|
||||
? D extends BuildPropType<T, V, C>
|
||||
? BuildPropOption<T, D, R, V, C>
|
||||
: never
|
||||
: never
|
||||
}
|
||||
>(
|
||||
props: O
|
||||
) =>
|
||||
fromPairs(
|
||||
Object.entries(props).map(([key, option]) => [
|
||||
key,
|
||||
buildProp(option as any, key),
|
||||
])
|
||||
) as unknown as {
|
||||
[K in keyof O]: O[K] extends { [propKey]: boolean }
|
||||
? O[K]
|
||||
: [O[K]] extends NativePropType
|
||||
? O[K]
|
||||
: O[K] extends BuildPropOption<
|
||||
infer T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
infer _D,
|
||||
infer R,
|
||||
infer V,
|
||||
infer C
|
||||
>
|
||||
? BuildPropReturn<T, O[K]['default'], R, V, C>
|
||||
: never
|
||||
}
|
||||
|
||||
export const definePropType = <T>(val: any) =>
|
||||
({ [wrapperKey]: val } as PropWrapper<T>)
|
3
packages/utils/vue/props/index.ts
Normal file
3
packages/utils/vue/props/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './util'
|
||||
export * from './types'
|
||||
export * from './runtime'
|
122
packages/utils/vue/props/runtime.ts
Normal file
122
packages/utils/vue/props/runtime.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { warn } from 'vue'
|
||||
import { fromPairs } from 'lodash-unified'
|
||||
import { isObject } from '../../types'
|
||||
import { hasOwn } from '../../objects'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type {
|
||||
EpProp,
|
||||
EpPropConvert,
|
||||
EpPropFinalized,
|
||||
EpPropInput,
|
||||
EpPropMergeType,
|
||||
IfEpProp,
|
||||
IfNativePropType,
|
||||
NativePropType,
|
||||
} from './types'
|
||||
|
||||
export const epPropKey = '__epPropKey'
|
||||
|
||||
export const definePropType = <T>(val: any): PropType<T> => val
|
||||
|
||||
export const isEpProp = (val: unknown): val is EpProp<any, any, any> =>
|
||||
isObject(val) && !!(val as any)[epPropKey]
|
||||
|
||||
/**
|
||||
* @description Build prop. It can better optimize prop types
|
||||
* @description 生成 prop,能更好地优化类型
|
||||
* @example
|
||||
// limited options
|
||||
// the type will be PropType<'light' | 'dark'>
|
||||
buildProp({
|
||||
type: String,
|
||||
values: ['light', 'dark'],
|
||||
} as const)
|
||||
* @example
|
||||
// limited options and other types
|
||||
// the type will be PropType<'small' | 'large' | number>
|
||||
buildProp({
|
||||
type: [String, Number],
|
||||
values: ['small', 'large'],
|
||||
validator: (val: unknown): val is number => typeof val === 'number',
|
||||
} as const)
|
||||
@link see more: https://github.com/element-plus/element-plus/pull/3341
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
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
|
164
packages/utils/vue/props/types.ts
Normal file
164
packages/utils/vue/props/types.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import type { epPropKey } from './runtime'
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
import type { IfNever, UnknownToNever, WritableArray } from './util'
|
||||
|
||||
type Value<T> = T[keyof T]
|
||||
|
||||
/**
|
||||
* Extract the type of a single prop
|
||||
*
|
||||
* 提取单个 prop 的参数类型
|
||||
*
|
||||
* @example
|
||||
* ExtractPropType<{ type: StringConstructor }> => string | undefined
|
||||
* ExtractPropType<{ type: StringConstructor, required: true }> => string
|
||||
* ExtractPropType<{ type: BooleanConstructor }> => boolean
|
||||
*/
|
||||
export type ExtractPropType<T extends object> = Value<
|
||||
ExtractPropTypes<{
|
||||
key: T
|
||||
}>
|
||||
>
|
||||
|
||||
/**
|
||||
* Extracts types via `ExtractPropTypes`, accepting `PropType<T>`, `XXXConstructor`, `never`...
|
||||
*
|
||||
* 通过 `ExtractPropTypes` 提取类型,接受 `PropType<T>`、`XXXConstructor`、`never`...
|
||||
*
|
||||
* @example
|
||||
* ResolvePropType<BooleanConstructor> => boolean
|
||||
* ResolvePropType<PropType<T>> => T
|
||||
**/
|
||||
export type ResolvePropType<T> = IfNever<
|
||||
T,
|
||||
never,
|
||||
ExtractPropType<{
|
||||
type: WritableArray<T>
|
||||
required: true
|
||||
}>
|
||||
>
|
||||
|
||||
/**
|
||||
* Merge Type, Value, Validator types
|
||||
* 合并 Type、Value、Validator 的类型
|
||||
*
|
||||
* @example
|
||||
* EpPropMergeType<StringConstructor, '1', 1> => 1 | "1" // ignores StringConstructor
|
||||
* EpPropMergeType<StringConstructor, never, number> => string | number
|
||||
*/
|
||||
export type EpPropMergeType<Type, Value, Validator> =
|
||||
| IfNever<UnknownToNever<Value>, ResolvePropType<Type>, never>
|
||||
| UnknownToNever<Value>
|
||||
| UnknownToNever<Validator>
|
||||
|
||||
/**
|
||||
* Handling default values for input (constraints)
|
||||
*
|
||||
* 处理输入参数的默认值(约束)
|
||||
*/
|
||||
export type EpPropInputDefault<
|
||||
Required extends boolean,
|
||||
Default
|
||||
> = Required extends true
|
||||
? never
|
||||
: Default extends Record<string, unknown> | Array<any>
|
||||
? () => Default
|
||||
: (() => Default) | Default
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* input prop `buildProp` or `buildProps` (constraints)
|
||||
*
|
||||
* prop 输入参数(约束)
|
||||
*
|
||||
* @example
|
||||
* EpPropInput<StringConstructor, 'a', never, never, true>
|
||||
* ⬇️
|
||||
* {
|
||||
type?: StringConstructor | undefined;
|
||||
required?: true | undefined;
|
||||
values?: readonly "a"[] | undefined;
|
||||
validator?: ((val: any) => boolean) | ((val: any) => val is never) | undefined;
|
||||
default?: undefined;
|
||||
}
|
||||
*/
|
||||
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>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }>
|
||||
|
||||
/**
|
||||
* Determine if it is `EpProp`
|
||||
*/
|
||||
export type IfEpProp<T, Y, N> = T extends { [epPropKey]: true } ? Y : N
|
||||
|
||||
/**
|
||||
* Converting input to output.
|
||||
*
|
||||
* 将输入转换为输出
|
||||
*/
|
||||
export type EpPropConvert<Input> = Input extends EpPropInput<
|
||||
infer Type,
|
||||
infer Value,
|
||||
infer Validator,
|
||||
any,
|
||||
infer Required
|
||||
>
|
||||
? EpPropFinalized<Type, Value, Validator, Input['default'], Required>
|
||||
: never
|
||||
|
||||
/**
|
||||
* Finalized conversion output
|
||||
*
|
||||
* 最终转换 EpProp
|
||||
*/
|
||||
export type EpPropFinalized<Type, Value, Validator, Default, Required> = EpProp<
|
||||
EpPropMergeType<Type, Value, Validator>,
|
||||
UnknownToNever<Default>,
|
||||
Required
|
||||
>
|
||||
|
||||
export {}
|
10
packages/utils/vue/props/util.ts
Normal file
10
packages/utils/vue/props/util.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type Writable<T> = { -readonly [P in keyof T]: T[P] }
|
||||
export type WritableArray<T> = T extends readonly any[] ? Writable<T> : T
|
||||
|
||||
export type IfNever<T, Y = true, N = false> = [T] extends [never] ? Y : N
|
||||
|
||||
export type IfUnknown<T, Y, N> = [unknown] extends [T] ? Y : N
|
||||
|
||||
export type UnknownToNever<T> = IfUnknown<T, never, T>
|
||||
|
||||
export {}
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"types": ["node", "jsdom", "vitest/globals"]
|
||||
"types": ["node", "jsdom"]
|
||||
},
|
||||
"include": ["packages", "vitest.setup.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
Loading…
Reference in New Issue
Block a user