From 7dd79e059b8ac903d59d1f684e37ac28b0b715c3 Mon Sep 17 00:00:00 2001 From: montmorill Date: Tue, 14 Apr 2026 22:52:48 +0800 Subject: [PATCH 1/4] Adds type safety for schema roles and refines generic signatures Introduces a `Roles` interface to define valid role strings for each schema type, enforcing type safety when setting the `role` meta property. Updates the `Schema` generic to include a `Type` parameter, allowing the `meta.role` to be constrained to the specific roles permitted for that schema type. This prevents invalid role assignments and improves developer experience with better IDE autocompletion and error checking. --- packages/core/src/index.ts | 156 +++++++++++++++++++++---------------- 1 file changed, 88 insertions(+), 68 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c8d8269..f6fc55b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,30 +38,30 @@ declare global { prototype: Schema resolve: Resolve from(source?: X): From - extend(type: string, resolve: Resolve): void - any(): Schema - never(): Schema - const(value: T): Schema - string(): Schema - number(): Schema - natural(): Schema - percent(): Schema - boolean(): Schema + extend(type: keyof Roles, resolve: Resolve): void + any(): Schema + never(): Schema + const(value: T): Schema + string(): Schema + number(): Schema + natural(): Schema + percent(): Schema + boolean(): Schema date(): Schema regExp(flag?: string): Schema arrayBuffer(): Schema arrayBuffer(encoding: 'hex' | 'base64'): Schema - bitset(bits: Partial>): Schema - function(): Schema any> - is(constructor: string): Schema - is(constructor: Constructor): Schema - array(inner: X): Schema[], TypeT[]> - dict = Schema>(inner: X, sKey?: Y): Schema, TypeS>, Dict, TypeT>> - tuple(list: X): Schema, TupleT> - object(dict: X): Schema, ObjectT> - union(list: readonly X[]): Schema, TypeT> - intersect(list: readonly X[]): Schema, IntersectT> - transform(inner: X, callback: (value: TypeS, options: Schemastery.Options) => T, preserve?: boolean): Schema, T> + bitset(bits: Partial>): Schema + function(): Schema any, 'function'> + is(constructor: string): Schema + is(constructor: Constructor): Schema + array(inner: X): Schema[], TypeT[], 'array'> + dict = Schema>(inner: X, sKey?: Y): Schema, TypeS>, Dict, TypeT>, 'dict'> + tuple(list: X): Schema, TupleT, 'tuple'> + object(dict: X): Schema, ObjectT, 'object'> + union(list: readonly X[]): Schema, TypeT, 'union'> + intersect(list: readonly X[]): Schema, IntersectT, 'intersect'> + transform(inner: X, callback: (value: TypeS, options: Schemastery.Options) => T, preserve?: boolean): Schema, T, 'transform'> lazy(callback: () => X): X ValidationError: typeof ValidationError } @@ -72,7 +72,27 @@ declare global { path?: (keyof any)[] } - export interface Meta { + export interface Roles { + lazy: never + any: never + never: never + const: never + string: 'datetime' | 'regexp' | 'textarea' + number: 'slider' + boolean: never + bitset: 'select' + function: never + is: never + array: 'select' | 'checkbox' | 'table' + dict: 'table' + tuple: never + object: never + union: 'radio' + intersect: never + transform: never + } + + export interface Meta { default?: T extends {} ? Partial : T required?: boolean disabled?: boolean @@ -80,7 +100,7 @@ declare global { badges?: { text: string; type: string }[] hidden?: boolean loose?: boolean - role?: string + role?: Type extends keyof Roles ? Roles[Type] : never extra?: any link?: string description?: string | Dict @@ -92,13 +112,13 @@ declare global { } } - interface Schemastery { + interface Schemastery { (data?: S | null, options?: Schemastery.Options): T - new (data?: S | null, options?: Schemastery.Options): T + new(data?: S | null, options?: Schemastery.Options): T [kSchema]: true uid: number - meta: Schemastery.Meta - type: string + meta: Schemastery.Meta + type: Type sKey?: Schema inner?: Schema list?: Schema[] @@ -112,28 +132,28 @@ declare global { preserve?: boolean '~standard': StandardSchemaV1.Props // toString(inline?: boolean): string - toJSON(): Schema - required(value?: boolean): Schema - hidden(value?: boolean): Schema - loose(value?: boolean): Schema - role(text: string, extra?: any): Schema - link(link: string): Schema - default(value: T): Schema - comment(text: string): Schema - description(text: string): Schema - disabled(value?: boolean): Schema - collapse(value?: boolean): Schema - deprecated(): Schema - experimental(): Schema - pattern(regexp: RegExp): Schema - max(value: number): Schema - min(value: number): Schema - step(value: number): Schema - set(key: string, value: Schema): Schema - push(value: Schema): Schema + toJSON(): Schema + required(value?: boolean): Schema + hidden(value?: boolean): Schema + loose(value?: boolean): Schema + role(role: this['meta']['role'], extra?: any): Schema + link(link: string): Schema + default(value: T): Schema + comment(text: string): Schema + description(text: string): Schema + disabled(value?: boolean): Schema + collapse(value?: boolean): Schema + deprecated(): Schema + experimental(): Schema + pattern(regexp: RegExp): Schema + max(value: number): Schema + min(value: number): Schema + step(value: number): Schema + set(key: string, value: Schema): Schema + push(value: Schema): Schema simplify(value?: any): any - i18n(messages: Dict): Schema - extra(key: K, value: Schemastery.Meta[K]): Schema + i18n(messages: Dict): Schema + extra(key: K, value: Schemastery.Meta[K]): Schema } } @@ -173,7 +193,7 @@ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true, }) -type Schema = Schemastery +type Schema = Schemastery const Schema = function (options: Schema) { const schema = function (data: any, options: Schemastery.Options = {}) { @@ -198,7 +218,7 @@ const Schema = function (options: Schema) { try { // eslint-disable-next-line no-new-func schema.callback = new Function('return ' + schema.callback)() - } catch {} + } catch { } } Object.defineProperty(schema, 'uid', { value: globalThis.__schemastery_index__++ }) Object.setPrototypeOf(schema, Schema.prototype) @@ -357,11 +377,11 @@ Schema.prototype.simplify = function simplify(this: Schema, value) { return result } else if (this.type === 'array' || this.type === 'tuple') { const result: any[] = [] - ;(value as any[]).forEach((value, index) => { - const schema = this.type === 'array' ? this.inner : this.list![index] - const item = schema ? schema.simplify(value) : value - result.push(item) - }) + ; (value as any[]).forEach((value, index) => { + const schema = this.type === 'array' ? this.inner : this.list![index] + const item = schema ? schema.simplify(value) : value + result.push(item) + }) return result } else if (this.type === 'intersect') { const result: Dict = {} @@ -374,7 +394,7 @@ Schema.prototype.simplify = function simplify(this: Schema, value) { try { Schema.resolve(value, schema, {}) return schema.simplify(value) - } catch {} + } catch { } } } return value @@ -384,7 +404,7 @@ Schema.prototype.toString = function toString(inline?: boolean) { return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>` } -Schema.prototype.role = function role(role, extra) { +Schema.prototype.role = function role(this: This, role: This['meta']['role'], extra?: any) { const schema = Schema(this) schema.meta = { ...schema.meta, role, extra } return schema @@ -481,7 +501,7 @@ Schema.date = function date() { if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options) return date }, true), - ]) + ]) as unknown as Schema } Schema.regExp = function regExp(flag = '') { @@ -737,15 +757,15 @@ Schema.extend('transform', (data, { inner, callback, preserve }, options) => { const [result, adapted = data] = Schema.resolve(data, inner!, options, true) if (preserve) { return [callback!(result)] - // } else if (isPlainObject(data)) { - // const temp: any = {} - // for (const key in result) { - // if (!(key in data)) continue - // temp[key] = data[key] - // delete data[key] - // } - // Object.assign(data, callback!(temp)) - // return [callback!(result)] + // } else if (isPlainObject(data)) { + // const temp: any = {} + // for (const key in result) { + // if (!(key in data)) continue + // temp[key] = data[key] + // delete data[key] + // } + // Object.assign(data, callback!(temp)) + // return [callback!(result)] } else { return [callback!(result), callback!(adapted)] } @@ -775,13 +795,13 @@ function defineMethod(name: string, keys: (keyof Schema)[], format: Formatter) { } case 'callback': { const callback = schema.callback = args[index] - ;callback['toJSON'] ||= () => callback.toString() + ; callback['toJSON'] ||= () => callback.toString() break } case 'constructor': { const constructor = schema.constructor = args[index] if (typeof constructor === 'function') { - ;constructor['toJSON'] ||= () => constructor['name'] + ; constructor['toJSON'] ||= () => constructor['name'] } break } From 27ae8ba52a9cadab8d6ed251a23b692767551c94 Mon Sep 17 00:00:00 2001 From: montmorill Date: Tue, 14 Apr 2026 23:00:44 +0800 Subject: [PATCH 2/4] Fix indentation in transform schema extension Corrects inconsistent indentation in commented-out code block to maintain consistent formatting throughout the file. --- packages/core/src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f6fc55b..e1919ec 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -757,15 +757,15 @@ Schema.extend('transform', (data, { inner, callback, preserve }, options) => { const [result, adapted = data] = Schema.resolve(data, inner!, options, true) if (preserve) { return [callback!(result)] - // } else if (isPlainObject(data)) { - // const temp: any = {} - // for (const key in result) { - // if (!(key in data)) continue - // temp[key] = data[key] - // delete data[key] - // } - // Object.assign(data, callback!(temp)) - // return [callback!(result)] + // } else if (isPlainObject(data)) { + // const temp: any = {} + // for (const key in result) { + // if (!(key in data)) continue + // temp[key] = data[key] + // delete data[key] + // } + // Object.assign(data, callback!(temp)) + // return [callback!(result)] } else { return [callback!(result), callback!(adapted)] } From ca979fe2edd71f474764f9f53bc6298657c297e5 Mon Sep 17 00:00:00 2001 From: montmorill Date: Tue, 14 Apr 2026 23:06:19 +0800 Subject: [PATCH 3/4] Fixes code formatting and removes unnecessary semicolons Standardizes code style by removing extraneous semicolons and fixing indentation in array iteration and property assignment patterns --- packages/core/src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e1919ec..502b7ea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -218,7 +218,7 @@ const Schema = function (options: Schema) { try { // eslint-disable-next-line no-new-func schema.callback = new Function('return ' + schema.callback)() - } catch { } + } catch {} } Object.defineProperty(schema, 'uid', { value: globalThis.__schemastery_index__++ }) Object.setPrototypeOf(schema, Schema.prototype) @@ -377,11 +377,11 @@ Schema.prototype.simplify = function simplify(this: Schema, value) { return result } else if (this.type === 'array' || this.type === 'tuple') { const result: any[] = [] - ; (value as any[]).forEach((value, index) => { - const schema = this.type === 'array' ? this.inner : this.list![index] - const item = schema ? schema.simplify(value) : value - result.push(item) - }) + ;(value as any[]).forEach((value, index) => { + const schema = this.type === 'array' ? this.inner : this.list![index] + const item = schema ? schema.simplify(value) : value + result.push(item) + }) return result } else if (this.type === 'intersect') { const result: Dict = {} @@ -394,7 +394,7 @@ Schema.prototype.simplify = function simplify(this: Schema, value) { try { Schema.resolve(value, schema, {}) return schema.simplify(value) - } catch { } + } catch {} } } return value @@ -795,13 +795,13 @@ function defineMethod(name: string, keys: (keyof Schema)[], format: Formatter) { } case 'callback': { const callback = schema.callback = args[index] - ; callback['toJSON'] ||= () => callback.toString() + callback.toJSON ||= () => callback.toString() break } case 'constructor': { const constructor = schema.constructor = args[index] if (typeof constructor === 'function') { - ; constructor['toJSON'] ||= () => constructor['name'] + constructor.toJSON ||= () => constructor.name } break } From 87dc834cc48ea2bc42abf8aa9adb6c843c6945f7 Mon Sep 17 00:00:00 2001 From: montmorill Date: Tue, 14 Apr 2026 23:07:08 +0800 Subject: [PATCH 4/4] Removes unnecessary type assertion from date schema The explicit type cast was redundant as TypeScript can infer the correct return type from the function implementation. --- packages/core/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 502b7ea..8bd6bb8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -501,7 +501,7 @@ Schema.date = function date() { if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options) return date }, true), - ]) as unknown as Schema + ]) } Schema.regExp = function regExp(flag = '') {