Skip to content

Instantly share code, notes, and snippets.

@b2whats
Created August 4, 2025 15:46
Show Gist options
  • Save b2whats/d7d11a9f76abde43d419dda271cb22d3 to your computer and use it in GitHub Desktop.
Save b2whats/d7d11a9f76abde43d419dda271cb22d3 to your computer and use it in GitHub Desktop.

Revisions

  1. b2whats created this gist Aug 4, 2025.
    472 changes: 472 additions & 0 deletions class-validator.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,472 @@
    /* eslint-disable @typescript-eslint/no-explicit-any */
    type FormMetadata = {
    fields: FieldsRules
    options?: {
    errorFieldName: string | symbol
    } & Required<ErrorField>
    touchedFields?: WeakMap<any, Set<string | symbol>>
    }
    type FieldsRules = Map<string | symbol, FieldsRule>
    type FieldsRule = {
    name: string | symbol
    value: any
    clonable: boolean
    size?: number
    validator: Validator
    }
    type ErrorField = {
    multiple?: boolean
    format?: 'object' | 'array'
    fieldsObserver?: (instance: any, callback: (key: string | symbol, value: any) => void) => void
    }

    const unknownValue = Symbol('unknownValue')
    // @ts-ignore
    Symbol.metadata ??= Symbol('Symbol.metadata')

    function Field<This, Value>(rules: ((v: Validator<Value, This>) => Validator) | ErrorField) {
    return (_: undefined, context: ClassFieldDecoratorContext<This, Value>) => {
    const { kind, name, metadata } = context

    if (kind !== 'field') throw new Error('Only fields can be decorated')

    const fields = (metadata.fields ??= new Map()) as FieldsRules
    const touchedFields = (metadata.touchedFields ??= new WeakMap()) as WeakMap<any, Set<string | symbol>>
    if (typeof rules === 'object') {
    const options = (metadata.options = {
    errorFieldName: name,
    multiple: rules.multiple ?? false,
    format: rules.format ?? 'object',
    })

    if (rules.fieldsObserver !== undefined) {
    context.addInitializer(function (this: any) {
    const { multiple, format } = options
    const onChangeFields = new Map<string | symbol, Validator>()

    for (const [key, { validator }] of fields) {
    if (validator.when === 'change') {
    onChangeFields.set(key, validator)
    }
    for (const fieldName of validator.dependent) {
    fields.get(fieldName)?.validator?.dependencies.add(key)
    }
    }

    if (onChangeFields.size === 0) return
    const touched = new Set<string | symbol>()
    touchedFields.set(this, touched)

    const setError = (key: string | symbol, error: FieldError | undefined) => {
    if (format === 'object') {
    this[name][key] = error && errorsToObject([error], multiple)[key]
    } else {
    this[name] = this[name].filter((error: FieldError) => error.property !== key)
    if (error) this[name].push(error)
    }
    }

    rules.fieldsObserver?.(this, (key, value) => {
    touched.add(key)
    const validator = onChangeFields.get(key)
    if (validator === undefined) return

    setError(key, validator.validate(value, this))

    for (const key of validator.dependencies) {
    const value = this[key]
    const validator = fields.get(key)?.validator
    if (validator === undefined || touched.has(key) === false) continue

    setError(key, validator.validate(value, this))
    }
    })
    })
    }

    return
    }

    const descriptor: FieldsRule = {
    name,
    value: unknownValue,
    clonable: true,
    validator: rules(new Validator(name)),
    }

    fields.set(name, descriptor)

    return function (value: any) {
    if (descriptor.value !== unknownValue) return value

    descriptor.size =
    value instanceof Map || value instanceof Set ? value.size : Array.isArray(value) ? value.length : undefined

    try {
    const clone = structuredClone(value)
    descriptor.value = clone
    descriptor.clonable = true
    } catch {
    descriptor.value = value
    descriptor.clonable = false
    }

    return value
    }
    }
    }
    type ValidateInstance = {
    [key: string | symbol]: any
    }

    type FormObjectErrors = Record<string | symbol, any>
    type FormErrors = FieldError[]

    function validate(instance: ValidateInstance): FormErrors | undefined {
    const metadata = instance?.constructor?.[Symbol.metadata] as FormMetadata | undefined
    const fields = metadata?.fields

    if (fields === undefined) return undefined

    let errors: FormErrors | undefined
    for (const [key, { validator }] of fields) {
    const value = instance[key]

    let error = validator.validate(value, instance)
    if (error !== undefined && validator.isNested === false) (errors ??= []).push(error)

    if (validator.isNested === false) continue

    if (error === undefined) error = { property: key, children: [] }

    if (value instanceof Set || Array.isArray(value)) {
    Array.from(value).forEach((item, index) => {
    const nestedError = validate(item)

    if (nestedError !== undefined) {
    ;(error.children ??= []).push({
    property: index,
    children: nestedError,
    })
    }
    })
    }

    if (value instanceof Map) {
    Array.from(value.entries()).forEach(([key, value]) => {
    const nestedError = validate(value)

    if (nestedError !== undefined) {
    ;(error.children ??= []).push({
    property: key,
    children: nestedError,
    })
    }
    })
    }

    if (value instanceof Object) {
    const nestedError = validate(value)

    if (nestedError !== undefined) {
    error.children = nestedError
    }
    }

    if (error.children?.length !== 0) (errors ??= []).push(error)
    }

    if (metadata?.options !== undefined) {
    const { errorFieldName, multiple, format } = metadata.options
    instance[errorFieldName] = format === 'object' ? errorsToObject(errors, multiple) : errors
    }

    return errors
    }

    function reset(instance: ValidateInstance): void {
    const metadata = instance?.constructor?.[Symbol.metadata] as FormMetadata | undefined
    const fields = metadata?.fields
    const options = metadata?.options
    const touchedFields = metadata?.touchedFields?.get(instance)

    if (fields === undefined) return

    for (const [key, { value, clonable, size }] of fields) {
    instance[key] = value

    if (clonable) continue

    if (Array.isArray(instance[key])) {
    instance[key] = instance[key].slice(0, size)
    for (const item of instance[key]) reset(item)
    } else if (instance[key] instanceof Set) {
    instance[key] = new Set(Array.from(instance[key].values()).slice(0, size))
    for (const item of instance[key]) reset(item)
    } else if (instance[key] instanceof Map) {
    const keysToRemove = Array.from(instance[key].keys()).slice(size)
    for (const key of keysToRemove) instance[key].delete(key)
    for (const [_, value] of instance[key]) reset(value)
    } else {
    reset(instance[key])
    }
    }
    if (options?.errorFieldName) instance[options.errorFieldName] = options.format === 'object' ? {} : []
    touchedFields?.clear()
    }

    function errorsToObject(errors: FormErrors | undefined, multiple: boolean): FormObjectErrors {
    if (errors === undefined) return {}

    const result: FormObjectErrors = {}

    const isArray = typeof errors[0]?.property === 'number'

    if (isArray) {
    const arrayResult: any[] = []
    for (const error of errors) {
    const index = error.property as number
    if (Array.isArray(error.children)) {
    arrayResult[index] = errorsToObject(error.children, multiple)
    }
    }
    return arrayResult
    }

    for (const error of errors) {
    if (Array.isArray(error.children)) {
    result[error.property] = errorsToObject(error.children, multiple)
    } else if (error.errors) {
    result[error.property] = multiple ? error.errors : error.errors[0]
    }
    }

    return result
    }

    type FieldError = {
    property: string | symbol | number
    errors?: string[]
    children?: FieldError[]
    }

    type Check<T = any> = (value: any, fields: T) => string | undefined

    class Validator<Value = any, Fields = any> {
    private property: string | symbol
    private checks: Check[] = []
    private isRequired = true
    private isCondition = false

    public when: 'change' | 'submit' = 'submit'
    public dependencies: Set<string | symbol> = new Set()
    public dependent: Set<string | symbol> = new Set()
    public isNested = false

    constructor(property: string | symbol) {
    this.property = property
    }

    optional(): this {
    this.isRequired = false
    return this
    }

    trigger(when: 'change' | 'submit', deps?: string[]): this {
    this.when = when
    if (deps !== undefined) {
    for (const dep of deps) {
    this.dependencies.add(dep)
    }
    }
    return this
    }

    nested(): this {
    this.isNested = true
    return this
    }

    string(message = 'Value must be a string'): this {
    this.checks.push((value) => (typeof value === 'string' ? undefined : message))
    return this
    }

    number(message = 'Value must be a number'): this {
    this.checks.push((value) => (typeof value === 'number' ? undefined : message))
    return this
    }

    array(message = 'Value must be an array'): this {
    this.checks.push((value) => (Array.isArray(value) ? undefined : message))
    return this
    }

    object(message = 'Value must be an object'): this {
    this.checks.push((value) => (typeof value === 'object' && value !== null ? undefined : message))
    return this
    }

    moreThan(limit: number, message?: string): this {
    this.checks.push((value) => {
    if (typeof value === 'number') {
    return value > limit ? undefined : (message ?? `Value must be greater than ${limit}`)
    }
    return undefined
    })
    return this
    }

    lessThan(limit: number, message?: string): this {
    this.checks.push((value) => {
    if (typeof value === 'number') {
    return value < limit ? undefined : (message ?? `Value must be less than ${limit}`)
    }
    return undefined
    })
    return this
    }

    lessOrEqualThanField(field: keyof Fields, message?: string): this {
    this.dependent.add(field as string | symbol)
    this.checks.push((value, fields) => {
    const fieldValue = fields[field]

    if (typeof value === 'number' && typeof fieldValue === 'number') {
    return value <= fieldValue ? undefined : (message ?? `Value must be less or equal than ${fieldValue}`)
    }
    return undefined
    })
    return this
    }

    min(limit: number, message?: string): this {
    this.checks.push((value) => {
    if (typeof value === 'number') {
    return value >= limit ? undefined : (message ?? `Value must be at least ${limit}`)
    }
    if (typeof value === 'string' || Array.isArray(value)) {
    return value.length >= limit ? undefined : (message ?? `Length must be at least ${limit}`)
    }
    if (value instanceof Set) {
    return value.size >= limit ? undefined : (message ?? `Size must be at least ${limit}`)
    }
    return undefined
    })
    return this
    }

    max(limit: number, message?: string): this {
    this.checks.push((value) => {
    if (typeof value === 'number') {
    return value <= limit ? undefined : (message ?? `Value must be at most ${limit}`)
    }
    if (typeof value === 'string' || Array.isArray(value)) {
    return value.length <= limit ? undefined : (message ?? `Length must be at most ${limit}`)
    }
    if (value instanceof Set) {
    return value.size <= limit ? undefined : (message ?? `Size must be at most ${limit}`)
    }
    return undefined
    })
    return this
    }

    oneOf(values: any[], message?: string): this {
    this.checks.push((value) => {
    if (values.includes(value)) return undefined
    return message ?? `Value must be one of ${values.join(', ')}`
    })
    return this
    }

    if(
    test: (fields: Fields, value: Value) => boolean,
    consequent: (v: Validator<Value, Fields>) => void,
    alternate?: (v: Validator<Value, Fields>) => void,
    ): this {
    this.isCondition = true
    const originalChecks = this.checks
    const originalNested = this.isNested
    const originalRequired = this.isRequired

    this.checks = []
    consequent(this)
    const thenChecks = this.checks
    const thenNested = this.isNested
    const thenRequired = this.isRequired

    let elseChecks: Check[] = []
    let elseRequired = (this.isRequired = originalRequired)
    let elseNested = (this.isNested = originalNested)
    if (alternate !== undefined) {
    this.checks = []
    alternate(this)
    elseChecks = this.checks
    elseRequired = this.isRequired
    elseNested = this.isNested
    }

    this.checks = originalChecks
    this.isRequired = originalRequired
    this.isNested = originalNested

    this.checks.push((value, fields) => {
    let checks: Check[] = []
    let required = originalRequired
    this.isNested = originalNested

    if (test(fields, value)) {
    checks = thenChecks
    required = thenRequired
    this.isNested = thenNested
    } else {
    checks = elseChecks
    required = elseRequired
    this.isNested = elseNested
    }

    if ([null, undefined].includes(value) && checks.length > 0) {
    if (required) return 'Required'
    return undefined
    }

    for (const check of checks) {
    const error = check(value, fields)

    if (error !== undefined) return error
    }

    return undefined
    })

    return this
    }

    custom(fn: (value: Value, fields: Fields) => string | undefined): this {
    this.checks.push(fn)
    return this
    }

    validate(value: any, fields: Fields): FieldError | undefined {
    if ([null, undefined].includes(value) && this.isCondition === false) {
    if (this.isRequired) return { property: this.property, errors: ['Required'] }
    return undefined
    }

    const fieldError: FieldError = {
    property: this.property,
    }

    for (const check of this.checks) {
    const error = check(value, fields)

    if (error === undefined) continue
    ;(fieldError.errors ??= []).push(error)
    }

    return fieldError.errors && fieldError
    }
    }

    export { Field, validate, reset }