diff --git a/packages/hooks/use-model-toggle/index.ts b/packages/hooks/use-model-toggle/index.ts index ba27b62f58..0d62164a2a 100644 --- a/packages/hooks/use-model-toggle/index.ts +++ b/packages/hooks/use-model-toggle/index.ts @@ -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 = { +export type UseModelTogglePropsRaw = { [K in T]: typeof _prop } & { [K in `onUpdate:${T}`]: typeof _event } +export type UseModelTogglePropsGeneric = { + [K in T]: ExtractPropType +} & { + [K in `onUpdate:${T}`]: ExtractPropType +} + export const createModelToggleComposable = (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 - - const useModelToggleEmits = [`update:${name}`] + [updateEventKeyRaw]: _event, + } as UseModelTogglePropsRaw const useModelToggle = ({ indicator, @@ -37,15 +46,12 @@ export const createModelToggleComposable = (name: T) => { onHide, }: ModelToggleParams) => { const instance = getCurrentInstance()! - const props = instance.props as _UseModelToggleProps & { + const { emit } = instance + const props = instance.props as UseModelTogglePropsGeneric & { 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 = (name: T) => { } } - watch(() => props[name], onChange as any) + watch(() => props[name], onChange) if ( shouldHideWhenRouteChanges && @@ -158,7 +164,7 @@ export const createModelToggleComposable = (name: T) => { } onMounted(() => { - onChange(props[name] as boolean) + onChange(props[name]) }) return { diff --git a/packages/utils/__tests__/vue/props.test.ts b/packages/utils/__tests__/vue/props.test.ts index 225e4fac7f..9b6ef67d2d 100644 --- a/packages/utils/__tests__/vue/props.test.ts +++ b/packages/utils/__tests__/vue/props.test.ts @@ -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>().toEqualTypeOf<[1, 2, 3]>() + expectTypeOf>>().toEqualTypeOf<{ + a: 'b' + }>() + expectTypeOf>().toEqualTypeOf<123>() + expectTypeOf< + Writable + >().not.toEqualTypeOf() + }) + + it('WritableArray', () => { + expectTypeOf>().toEqualTypeOf<[1, 2, 3]>() + expectTypeOf< + WritableArray + >().toEqualTypeOf() + }) + + it('IfNever', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + it('UnknownToNever', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<1>() + }) + + it('ResolvePropType', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ResolvePropType<[DateConstructor, NumberConstructor]> + >().toEqualTypeOf() + expectTypeOf< + ResolvePropType + >().toEqualTypeOf() + expectTypeOf< + ResolvePropType> + >().toEqualTypeOf() + expectTypeOf>().toBeNever() + }) + + it('EpPropMergeType', () => { + expectTypeOf< + EpPropMergeType + >().toEqualTypeOf<'str' | 1>() + expectTypeOf>().toEqualTypeOf< + 2 | 3 | 4 + >() + }) + + it('EpPropInputDefault', () => { + expectTypeOf>().toBeNever() + expectTypeOf>().toEqualTypeOf<1 | (() => 1)>() + }) + + it('EpProp', () => { + expectTypeOf>().toEqualTypeOf<{ + readonly type: PropType<'1'> + readonly required: false + readonly validator: ((val: unknown) => boolean) | undefined + readonly default: '2' + [epPropKey]: true + }>() + + expectTypeOf>().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(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 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 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 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 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 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() @@ -409,17 +486,17 @@ describe('buildProps', () => { expectTypeOf(props.key12).toEqualTypeOf<{ readonly type: PropType readonly required: false - readonly default: undefined readonly validator: ((val: unknown) => boolean) | undefined - [propKey]: true + [epPropKey]: true }>() expectTypeOf(props.key13).toEqualTypeOf<{ readonly type: PropType 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 readonly required: false - readonly default: '123' + // TODO + readonly default: () => '123' readonly validator: ((val: unknown) => boolean) | undefined - [propKey]: true + [epPropKey]: true }>() }) }) diff --git a/packages/utils/vue/props.ts b/packages/utils/vue/props.ts deleted file mode 100644 index 679a78e87a..0000000000 --- a/packages/utils/vue/props.ts +++ /dev/null @@ -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 = { [wrapperKey]: T } - -export const propKey = '__elPropsReservedKey' - -type ResolveProp = ExtractPropTypes<{ - key: { type: T; required: true } -}>['key'] -type ResolvePropType = ResolveProp extends { type: infer V } - ? V - : ResolveProp -type ResolvePropTypeWithReadonly = Readonly extends Readonly< - Array -> - ? ResolvePropType - : ResolvePropType - -type IfUnknown = [unknown] extends [T] ? V : T - -export type BuildPropOption, R, V, C> = { - type?: T - values?: readonly V[] - required?: R - default?: R extends true - ? never - : D extends Record | Array - ? () => D - : (() => D) | D - validator?: ((val: any) => val is C) | ((val: any) => boolean) -} - -type _BuildPropType = - | (T extends PropWrapper - ? T[typeof wrapperKey] - : [V] extends [never] - ? ResolvePropTypeWithReadonly - : never) - | V - | C -export type BuildPropType = _BuildPropType< - IfUnknown, - IfUnknown, - IfUnknown -> - -type _BuildPropDefault = [T] extends [ - // eslint-disable-next-line @typescript-eslint/ban-types - Record | Array | Function -] - ? D - : D extends () => T - ? ReturnType - : D - -export type BuildPropDefault = R extends true - ? { readonly default?: undefined } - : { - readonly default: Exclude extends never - ? undefined - : Exclude<_BuildPropDefault, undefined> - } -export type BuildPropReturn = { - readonly type: PropType> - readonly required: IfUnknown - readonly validator: ((val: unknown) => boolean) | undefined - [propKey]: true -} & BuildPropDefault< - BuildPropType, - IfUnknown, - IfUnknown -> - -/** - * @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 = never, - R extends boolean = false, - V = never, - C = never ->( - option: BuildPropOption, - key?: string -): BuildPropReturn { - // 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 -} - -type NativePropType = [ - ((...args: any) => any) | { new (...args: any): any } | undefined | null -] - -export const buildProps = < - O extends { - [K in keyof O]: O[K] extends BuildPropReturn - ? 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 - ? BuildPropOption - : 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 - : never - } - -export const definePropType = (val: any) => - ({ [wrapperKey]: val } as PropWrapper) diff --git a/packages/utils/vue/props/index.ts b/packages/utils/vue/props/index.ts new file mode 100644 index 0000000000..6a2f4253ed --- /dev/null +++ b/packages/utils/vue/props/index.ts @@ -0,0 +1,3 @@ +export * from './util' +export * from './types' +export * from './runtime' diff --git a/packages/utils/vue/props/runtime.ts b/packages/utils/vue/props/runtime.ts new file mode 100644 index 0000000000..0b742bf801 --- /dev/null +++ b/packages/utils/vue/props/runtime.ts @@ -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 = (val: any): PropType => val + +export const isEpProp = (val: unknown): val is EpProp => + 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 = never, + Required extends boolean = false +>( + prop: EpPropInput, + key?: string +): EpPropFinalized => { + // 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 + > +>( + props: Props +): { + [K in keyof Props]: IfEpProp< + Props[K], + Props[K], + IfNativePropType> + > +} => + fromPairs( + Object.entries(props).map(([key, option]) => [ + key, + buildProp(option as any, key), + ]) + ) as any diff --git a/packages/utils/vue/props/types.ts b/packages/utils/vue/props/types.ts new file mode 100644 index 0000000000..04a7c94c1c --- /dev/null +++ b/packages/utils/vue/props/types.ts @@ -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[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 = Value< + ExtractPropTypes<{ + key: T + }> +> + +/** + * Extracts types via `ExtractPropTypes`, accepting `PropType`, `XXXConstructor`, `never`... + * + * 通过 `ExtractPropTypes` 提取类型,接受 `PropType`、`XXXConstructor`、`never`... + * + * @example + * ResolvePropType => boolean + * ResolvePropType> => T + **/ +export type ResolvePropType = IfNever< + T, + never, + ExtractPropType<{ + type: WritableArray + required: true + }> +> + +/** + * Merge Type, Value, Validator types + * 合并 Type、Value、Validator 的类型 + * + * @example + * EpPropMergeType => 1 | "1" // ignores StringConstructor + * EpPropMergeType => string | number + */ +export type EpPropMergeType = + | IfNever, ResolvePropType, never> + | UnknownToNever + | UnknownToNever + +/** + * Handling default values for input (constraints) + * + * 处理输入参数的默认值(约束) + */ +export type EpPropInputDefault< + Required extends boolean, + Default +> = Required extends true + ? never + : Default extends Record | Array + ? () => 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] extends [NativePropType] ? Y : N + +/** + * input prop `buildProp` or `buildProps` (constraints) + * + * prop 输入参数(约束) + * + * @example + * EpPropInput + * ⬇️ + * { + 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, + Required extends boolean +> = { + type?: Type + required?: Required + values?: readonly Value[] + validator?: ((val: any) => val is Validator) | ((val: any) => boolean) + default?: EpPropInputDefault +} + +/** + * 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 = { + readonly type: PropType + readonly required: [Required] extends [true] ? true : false + readonly validator: ((val: unknown) => boolean) | undefined + [epPropKey]: true +} & IfNever + +/** + * Determine if it is `EpProp` + */ +export type IfEpProp = T extends { [epPropKey]: true } ? Y : N + +/** + * Converting input to output. + * + * 将输入转换为输出 + */ +export type EpPropConvert = Input extends EpPropInput< + infer Type, + infer Value, + infer Validator, + any, + infer Required +> + ? EpPropFinalized + : never + +/** + * Finalized conversion output + * + * 最终转换 EpProp + */ +export type EpPropFinalized = EpProp< + EpPropMergeType, + UnknownToNever, + Required +> + +export {} diff --git a/packages/utils/vue/props/util.ts b/packages/utils/vue/props/util.ts new file mode 100644 index 0000000000..01c41a105c --- /dev/null +++ b/packages/utils/vue/props/util.ts @@ -0,0 +1,10 @@ +export type Writable = { -readonly [P in keyof T]: T[P] } +export type WritableArray = T extends readonly any[] ? Writable : T + +export type IfNever = [T] extends [never] ? Y : N + +export type IfUnknown = [unknown] extends [T] ? Y : N + +export type UnknownToNever = IfUnknown + +export {} diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json index c1c2485bea..f96be8b234 100644 --- a/tsconfig.vitest.json +++ b/tsconfig.vitest.json @@ -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"]