refactor(utils): ep prop (#8018)

This commit is contained in:
三咲智子 2022-06-01 16:00:27 +08:00 committed by GitHub
parent 6359538a45
commit 362c7f6b3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 444 additions and 268 deletions

View File

@ -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 {

View File

@ -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
}>()
})
})

View File

@ -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>)

View File

@ -0,0 +1,3 @@
export * from './util'
export * from './types'
export * from './runtime'

View 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

View 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
* TypeValueValidator
*
* @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 {}

View 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 {}

View File

@ -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"]