Skip to content

Improve types of class mixins #48

@blikblum

Description

@blikblum

withValidation and withComputed has types that does not work declaring descendants

This is the sonnet proposed code for a class mixin:

Prompt
You are a coding assistant

The language is typescript

Create a withValidation class mixin that extends a Model class adding support to a static property validation

// Base Model class
class Model {
  constructor(public data: Record<string, any> = {}) {}
  
  toJSON() {
    return { ...this.data };
  }
}

// Type definitions for validation
type ValidationRule = {
  validator: (value: any) => boolean;
  message: string;
};

type ValidationRules = {
  [field: string]: ValidationRule[];
};

type ValidationError = {
  field: string;
  messages: string[];
};

// Validation mixin constructor type
type ModelConstructor = new (...args: any[]) => Model;

// Define a interface for the class with validation
interface WithValidation {
  validation: ValidationRules;
}

/**
 * Mixin that adds validation capabilities to a Model class
 */
function withValidation<T extends ModelConstructor>(BaseClass: T) {
  // Create a class that includes validation
  return class extends BaseClass {
    // Static validation property to be overridden by child classes
    static validation: ValidationRules = {};

    /**
     * Validates the model data against defined validation rules
     * @returns An array of validation errors or null if valid
     */
    validate(): ValidationError[] | null {
      // Access the constructor and safely cast it to include the validation property
      const validationRules = (this.constructor as unknown as WithValidation).validation;
      const errors: ValidationError[] = [];

      // Loop through each validation rule
      for (const [field, rules] of Object.entries(validationRules)) {
        const value = this.data[field];
        const fieldErrors: string[] = [];

        // Apply each rule to the field
        for (const rule of rules) {
          if (!rule.validator(value)) {
            fieldErrors.push(rule.message);
          }
        }

        if (fieldErrors.length > 0) {
          errors.push({
            field,
            messages: fieldErrors
          });
        }
      }

      return errors.length > 0 ? errors : null;
    }

    /**
     * Checks if the model is valid
     */
    isValid(): boolean {
      return this.validate() === null;
    }
  };
}

// Common validators
const validators = {
  required: (value: any) => value !== undefined && value !== null && value !== '',
  minLength: (length: number) => (value: string) => typeof value === 'string' && value.length >= length,
  maxLength: (length: number) => (value: string) => typeof value === 'string' && value.length <= length,
  isEmail: (value: string) => typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  isNumber: (value: any) => typeof value === 'number' && !isNaN(value)
};

// Example User model with validation
class User extends withValidation(Model) {
  static override validation: ValidationRules = {
    name: [
      { validator: validators.required, message: 'Name is required' },
      { validator: validators.minLength(2), message: 'Name must be at least 2 characters' }
    ],
    email: [
      { validator: validators.required, message: 'Email is required' },
      { validator: validators.isEmail, message: 'Email format is invalid' }
    ],
    age: [
      { validator: validators.isNumber, message: 'Age must be a number' }
    ]
  };

  constructor(data: Record<string, any> = {}) {
    super(data);
  }
}

// Example usage
const validUser = new User({
  name: 'John Doe',
  email: 'john@example.com',
  age: 30
});

const invalidUser = new User({
  name: 'J',
  email: 'not-an-email'
});

console.log('Valid user is valid:', validUser.isValid()); // true
console.log('Invalid user is valid:', invalidUser.isValid()); // false
console.log('Invalid user validation errors:', invalidUser.validate());

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions