refactor: improve buildProp (#3558)

* refactor: improve buildProp

* fix: fix key

* fix: improve validator
This commit is contained in:
三咲智子 2021-09-22 20:27:23 +08:00 committed by GitHub
parent 35c90180d1
commit 46d69bd37f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 369 additions and 65 deletions

View File

@ -106,6 +106,7 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.0.0-beta.0",
"esno": "^0.9.1",
"expect-type": "^0.12.0",
"fast-glob": "^3.2.7",
"file-save": "^0.2.0",
"gulp": "^4.0.2",

View File

@ -1,13 +1,13 @@
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
import type { ZIndexProperty } from 'csstype'
export const affixProps = {
zIndex: buildProp<ZIndexProperty>({
type: [Number, String],
zIndex: buildProp({
type: definePropType<ZIndexProperty>([Number, String]),
default: 100,
}),
} as const),
target: {
type: String,
default: '',

View File

@ -1,5 +1,6 @@
import { buildProp } from '@element-plus/utils/props'
import type { CSSProperties, ExtractPropTypes } from 'vue'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
import type { ObjectFitProperty } from 'csstype'
export const avatarProps = {
size: buildProp({
@ -22,8 +23,8 @@ export const avatarProps = {
},
alt: String,
srcSet: String,
fit: buildProp<CSSProperties['objectFit']>({
type: String,
fit: buildProp({
type: definePropType<ObjectFitProperty>(String),
default: 'cover',
} as const),
} as const

View File

@ -3,10 +3,10 @@ import { buildProp } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
export const badgeProps = {
value: buildProp<string | number>({
value: buildProp({
type: [String, Number],
default: '',
}),
} as const),
max: {
type: Number,
default: 99,

View File

@ -1,13 +1,13 @@
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
export const breadcrumbItemProps = {
to: buildProp<RouteLocationRaw>({
type: [String, Object],
to: buildProp({
type: definePropType<RouteLocationRaw>([String, Object]),
default: '',
}),
} as const),
replace: {
type: Boolean,
default: false,

View File

@ -1,4 +1,4 @@
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
import type { StyleValue } from '@element-plus/utils/types'
@ -7,10 +7,10 @@ export const cardProps = {
type: String,
default: '',
},
bodyStyle: buildProp<StyleValue>({
type: [String, Object, Array],
bodyStyle: buildProp({
type: definePropType<StyleValue>([String, Object, Array]),
default: '',
}),
} as const),
shadow: {
type: String,
default: '',

View File

@ -1,5 +1,5 @@
import { isValidWidthUnit } from '@element-plus/utils/validators'
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import type { ExtractPropTypes } from 'vue'
@ -9,8 +9,8 @@ export const dialogProps = {
type: Boolean,
default: false,
},
beforeClose: buildProp<(...args: any[]) => void>({
type: Function,
beforeClose: buildProp({
type: definePropType<(...args: any[]) => void>(Function),
}),
destroyOnClose: {
type: Boolean,
@ -68,7 +68,7 @@ export const dialogProps = {
required: true,
},
modalClass: String,
width: buildProp<string | number>({
width: buildProp({
type: [String, Number],
validator: isValidWidthUnit,
}),

View File

@ -1,4 +1,4 @@
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { VNode, ExtractPropTypes } from 'vue'
@ -29,12 +29,12 @@ export const messageProps = {
type: String,
default: '',
},
message: buildProp<string | VNode>({
type: [String, Object],
message: buildProp({
type: definePropType<string | VNode>([String, Object]),
default: '',
}),
onClose: buildProp<() => void>({
type: Function,
} as const),
onClose: buildProp({
type: definePropType<() => void>(Function),
required: false,
}),
showClose: {

View File

@ -1,4 +1,4 @@
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { VNode, ExtractPropTypes } from 'vue'
@ -30,20 +30,20 @@ export const notificationProps = {
type: String,
default: '',
},
message: buildProp<string | VNode>({
type: [String, Object],
message: buildProp({
type: definePropType<string | VNode>([String, Object]),
default: '',
}),
offset: {
type: Number,
default: 0,
},
onClick: buildProp<() => void>({
type: Function,
onClick: buildProp({
type: definePropType<() => void>(Function),
default: () => undefined,
}),
onClose: buildProp<() => void, boolean>({
type: Function,
onClose: buildProp({
type: definePropType<() => void>(Function),
required: true,
}),
position: buildProp({

View File

@ -1,9 +1,10 @@
import { createVNode, defineComponent, renderSlot, h } from 'vue'
import { PatchFlags } from '@element-plus/utils/vnode'
import { useSameTarget } from '@element-plus/hooks'
import { buildProp } from '@element-plus/utils/props'
import { buildProp, definePropType } from '@element-plus/utils/props'
import type { ExtractPropTypes, CSSProperties } from 'vue'
import type { ZIndexProperty } from 'csstype'
export const overlayProps = {
mask: {
@ -14,11 +15,15 @@ export const overlayProps = {
type: Boolean,
default: false,
},
overlayClass: buildProp<string | string[] | Record<string, boolean>>({
type: [String, Array, Object],
overlayClass: buildProp({
type: definePropType<string | string[] | Record<string, boolean>>([
String,
Array,
Object,
]),
}),
zIndex: buildProp<CSSProperties['zIndex']>({
type: [String, Number],
zIndex: buildProp({
type: definePropType<ZIndexProperty>([String, Number]),
}),
} as const
export type OverlayProps = ExtractPropTypes<typeof overlayProps>

View File

@ -22,20 +22,19 @@ import { defineComponent, watch, computed, ref } from 'vue'
import isEqual from 'lodash/isEqual'
import { ElSelect, ElOption } from '@element-plus/components/select'
import { useLocaleInject } from '@element-plus/hooks'
import { buildProp, mutable } from '@element-plus/utils/props'
import { buildProp, definePropType, mutable } from '@element-plus/utils/props'
import { usePagination } from '../usePagination'
import type { Nullable } from '@element-plus/utils/types'
const defaultPageSizes = mutable([10, 20, 30, 40, 50, 100] as const)
const paginationSizesProps = {
pageSize: {
type: Number,
required: true,
},
pageSizes: buildProp<number[], false, typeof defaultPageSizes>({
type: Array,
default: () => defaultPageSizes,
pageSizes: buildProp({
type: definePropType<number[]>(Array),
default: () => mutable([10, 20, 30, 40, 50, 100] as const),
} as const),
popperClass: {
type: String,

View File

@ -9,7 +9,7 @@ import {
} from 'vue'
import { useLocaleInject } from '@element-plus/hooks'
import { debugWarn } from '@element-plus/utils/error'
import { buildProp, mutable } from '@element-plus/utils/props'
import { buildProp, definePropType, mutable } from '@element-plus/utils/props'
import { elPaginationKey } from '@element-plus/tokens'
import Prev from './components/prev.vue'
@ -38,8 +38,6 @@ type LayoutKey =
| 'sizes'
| 'slot'
const defaultPageSizes = mutable([10, 20, 30, 40, 50, 100] as const)
export const paginationProps = {
total: Number,
pageSize: Number,
@ -66,9 +64,9 @@ export const paginationProps = {
['prev', 'pager', 'next', 'jumper', '->', 'total'] as LayoutKey[]
).join(', '),
},
pageSizes: buildProp<number[], false, typeof defaultPageSizes>({
type: Array,
default: () => defaultPageSizes,
pageSizes: buildProp({
type: definePropType<number[]>(Array),
default: () => mutable([10, 20, 30, 40, 50, 100] as const),
}),
popperClass: {
type: String,

View File

@ -1,5 +1,21 @@
import { debugWarn } from './error'
import type { ExtractPropTypes, PropType } from '@vue/runtime-core'
import type { Mutable } from './types'
import type { PropType } from 'vue'
const wrapperKey = Symbol()
export type PropWrapper<T> = { [wrapperKey]: T }
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>
/**
* @description Build prop. It can better optimize prop types
@ -22,9 +38,14 @@ import type { PropType } from 'vue'
@link see more: https://github.com/element-plus/element-plus/pull/3341
*/
export function buildProp<
T = any,
T = never,
D extends
| (T extends PropWrapper<any>
? T[typeof wrapperKey]
: ResolvePropTypeWithReadonly<T>)
| V = never,
R extends boolean = false,
D extends T = T,
V = never,
C = never
>({
values,
@ -33,44 +54,72 @@ export function buildProp<
type,
validator,
}: {
values?: readonly T[]
type?: T
values?: readonly V[]
required?: R
default?: R extends true
? never
: D extends Record<string, unknown> | Array<any>
? () => D
: D
type?: any
validator?: ((val: any) => val is C) | ((val: any) => boolean)
} = {}) {
type DefaultType = typeof defaultValue
type HasDefaultValue = Exclude<T, D> extends never ? false : true
type HasDefaultValue = Exclude<D, undefined> extends never ? false : true
type Type = PropType<
| (T extends PropWrapper<unknown>
? T[typeof wrapperKey]
: [V] extends [never]
? ResolvePropTypeWithReadonly<T>
: never)
| V
| C
>
return {
type: type as PropType<T | C>,
type: ((type as any)?.[wrapperKey] || type) as unknown as Type,
required: !!required as R,
default: defaultValue as R extends true
default: defaultValue as unknown as R extends true
? never
: HasDefaultValue extends true
? Exclude<DefaultType, undefined>
? Exclude<
D extends Record<string, unknown> | Array<any> ? () => D : D,
undefined
>
: undefined,
validator:
values || validator
? (val: unknown) => {
let valid = false
if (values)
valid ||= ([...values, defaultValue] as unknown[]).includes(val)
let allowedValues: unknown[] = []
if (values) {
allowedValues = [...values, defaultValue]
valid ||= allowedValues.includes(val)
}
if (validator) valid ||= validator(val)
if (!valid && allowedValues.length > 0) {
debugWarn(
`Vue warn`,
`Invalid prop: Expected one of (${allowedValues.join(
', '
)}), got value ${val}`
)
}
return valid
}
: undefined,
} as const
}
export const definePropType = <T>(val: any) =>
({ [wrapperKey]: val } as PropWrapper<T>)
export const keyOf = <T>(arr: T) => Object.keys(arr) as Array<keyof T>
export const mutable = <T extends readonly any[]>(val: T) =>
val as Mutable<typeof val>
export const mutable = <T extends readonly any[] | Record<string, unknown>>(
val: T
) => val as Mutable<typeof val>
export const componentSize = ['large', 'medium', 'small', 'mini'] as const

View File

@ -0,0 +1,246 @@
import { expectTypeOf } from 'expect-type'
import { buildProp, definePropType, mutable, keyOf } from '../props'
import type { PropType } from 'vue'
describe('buildProp', () => {
it('Only type', () => {
expectTypeOf(
buildProp({
type: definePropType<'a' | 'b'>(String),
})
).toEqualTypeOf<{
readonly type: PropType<'a' | 'b'>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Only values', () => {
expectTypeOf(
buildProp({
values: [1, 2, 3, 4],
} as const)
).toEqualTypeOf<{
readonly type: PropType<1 | 2 | 3 | 4>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type and values', () => {
expectTypeOf(
buildProp({
type: definePropType<number[]>(Array),
values: [1, 2, 3, 4],
} as const)
).toEqualTypeOf<{
readonly type: PropType<1 | 2 | 3 | 4 | number[]>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Values and validator', () => {
expectTypeOf(
buildProp({
values: ['a', 'b', 'c'],
validator: (val: unknown): val is number => typeof val === 'number',
} as const)
).toEqualTypeOf<{
readonly type: PropType<number | 'a' | 'b' | 'c'>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Values and required', () => {
expectTypeOf(
buildProp({
values: ['a', 'b', 'c'],
required: true,
} as const)
).toEqualTypeOf<{
readonly type: PropType<'a' | 'b' | 'c'>
readonly required: true
readonly default: never
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Value and default', () => {
expectTypeOf(
buildProp({
values: ['a', 'b', 'c'],
required: false,
default: 'b',
} as const)
).toEqualTypeOf<{
readonly type: PropType<'a' | 'b' | 'c'>
readonly required: false
readonly default: 'b'
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type and Array default value', () => {
expectTypeOf(
buildProp({
type: definePropType<string[]>(Array),
default: () => mutable(['a', 'b'] as const),
} as const)
).toEqualTypeOf<{
readonly type: PropType<string[]>
readonly required: false
readonly default: () => ['a', 'b']
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type and Object default value', () => {
interface Options {
key: string
}
expectTypeOf(
buildProp({
type: definePropType<Options>(Object),
default: () => mutable({ key: 'value' } as const),
} as const)
).toEqualTypeOf<{
readonly type: PropType<Options>
readonly required: false
readonly default: () => { key: 'value' }
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type, validator and Object default value', () => {
interface Options {
key: string
}
expectTypeOf(
buildProp({
type: definePropType<Options>(Object),
default: () => ({ key: 'value' }),
validator: (val: unknown): val is string => true,
} as const)
).toEqualTypeOf<{
readonly type: PropType<string | Options>
readonly required: false
readonly default: () => { key: string }
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type, validator, required', () => {
expectTypeOf(
buildProp({
type: definePropType<'a' | 'b' | 'c'>(String),
required: true,
validator: (val: unknown): val is number => true,
} as const)
).toEqualTypeOf<{
readonly type: PropType<number | 'a' | 'b' | 'c'>
readonly required: true
readonly default: never
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Normal type', () => {
expectTypeOf(
buildProp({
type: String,
})
).toEqualTypeOf<{
readonly type: PropType<string>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Normal types', () => {
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
}>()
})
it('Normal type and values', () => {
expectTypeOf(
buildProp({
type: String,
values: ['1', '2', '3'],
} as const)
).toEqualTypeOf<{
readonly type: PropType<'1' | '2' | '3'>
readonly required: false
readonly default: undefined
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Required and validator', () => {
expectTypeOf(
buildProp({
required: true,
validator: (val: unknown): val is string => true,
} as const)
).toEqualTypeOf<{
readonly type: PropType<string>
readonly required: true
readonly default: never
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Required and validator', () => {
expectTypeOf(
buildProp({
values: keyOf({ a: 'a', b: 'b' }),
default: 'a',
} as const)
).toEqualTypeOf<{
readonly type: PropType<'a' | 'b'>
readonly required: false
readonly default: 'a'
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type and default value', () => {
expectTypeOf(
buildProp({
type: definePropType<{ key: 'a' | 'b' | 'c' } | undefined>(Object),
default: () => mutable({ key: 'a' } as const),
} as const)
).toEqualTypeOf<{
readonly type: PropType<{ key: 'a' | 'b' | 'c' } | undefined>
readonly required: false
readonly default: () => { key: 'a' }
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
it('Type and default value', () => {
expectTypeOf(
buildProp({
type: [String, Number],
default: '',
} as const)
).toEqualTypeOf<{
readonly type: PropType<string | number>
readonly required: false
readonly default: ''
readonly validator: ((val: unknown) => boolean) | undefined
}>()
})
})

View File

@ -6219,6 +6219,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
expect-type@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.12.0.tgz#133534b5e2561158c371e74af63fd8f18a9f3d42"
integrity sha512-IHwziEOjpjXqxQhtOAD5zMiQpGztaEKM4Q8wnwoRN9NIFlnyNHNjRxKWv+18UqRfsqi6vVnZIYFU16ePf+HaqA==
expect@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417"