Skip to content

Creating Custom Validators

While Phaser Hooks provides built-in validators, you can create your own custom validators to meet specific validation needs.

A validator function must follow this signature:

type StateValidator = (value: unknown) => boolean | string;
  • Input: value - The value to validate
  • Output: true if valid, false or string error message if invalid
import { withLocalState } from 'phaser-hooks';
// Custom validator for email format
function emailValidator(value: unknown): boolean | string {
const email = value as string;
if (typeof email !== 'string') {
return 'Value must be a string';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return 'Value must be a valid email address';
}
return true;
}
// Custom validator for positive numbers
function positiveNumberValidator(value: unknown): boolean | string {
const num = value as number;
if (typeof num !== 'number' || Number.isNaN(num)) {
return 'Value must be a number';
}
if (num <= 0) {
return 'Value must be a positive number';
}
return true;
}
export class GameScene extends Phaser.Scene {
create() {
// Use custom validators
const playerEmail = withLocalState(this, 'email', 'player@game.com', {
validator: emailValidator
});
const playerScore = withLocalState(this, 'score', 100, {
validator: positiveNumberValidator
});
}
}
// Validator factory for string length
function stringLengthValidator(minLength: number, maxLength?: number) {
return (value: unknown): boolean | string => {
const str = value as string;
if (typeof str !== 'string') {
return 'Value must be a string';
}
if (str.length < minLength) {
return `String must be at least ${minLength} characters long`;
}
if (maxLength !== undefined && str.length > maxLength) {
return `String must be at most ${maxLength} characters long`;
}
return true;
};
}
// Validator factory for object properties
function objectPropertyValidator(requiredProps: string[]) {
return (value: unknown): boolean | string => {
if (typeof value !== 'object' || value === null) {
return 'Value must be an object';
}
const obj = value as Record<string, any>;
for (const prop of requiredProps) {
if (!(prop in obj)) {
return `Object must have property: ${prop}`;
}
}
return true;
};
}
export class GameScene extends Phaser.Scene {
create() {
// Use validator factories
const playerName = withLocalState(this, 'playerName', 'Player', {
validator: stringLengthValidator(3, 20)
});
const playerData = withLocalState(this, 'playerData', { name: 'Player', level: 1 }, {
validator: objectPropertyValidator(['name', 'level'])
});
}
}
// Complex validator for game coordinates
function coordinateValidator(maxX: number, maxY: number) {
return (value: unknown): boolean | string => {
if (typeof value !== 'object' || value === null) {
return 'Value must be an object';
}
const coord = value as { x: number; y: number };
if (typeof coord.x !== 'number' || typeof coord.y !== 'number') {
return 'Coordinates must have numeric x and y properties';
}
if (coord.x < 0 || coord.x > maxX) {
return `X coordinate must be between 0 and ${maxX}`;
}
if (coord.y < 0 || coord.y > maxY) {
return `Y coordinate must be between 0 and ${maxY}`;
}
return true;
};
}
// Validator for inventory items
function inventoryItemValidator() {
return (value: unknown): boolean | string => {
if (typeof value !== 'object' || value === null) {
return 'Value must be an object';
}
const item = value as { id: string; name: string; quantity: number };
if (typeof item.id !== 'string' || item.id.trim() === '') {
return 'Item must have a valid ID';
}
if (typeof item.name !== 'string' || item.name.trim() === '') {
return 'Item must have a valid name';
}
if (typeof item.quantity !== 'number' || item.quantity < 0) {
return 'Item quantity must be a non-negative number';
}
return true;
};
}
export class GameScene extends Phaser.Scene {
create() {
const playerPosition = withLocalState(this, 'position', { x: 0, y: 0 }, {
validator: coordinateValidator(800, 600)
});
const inventoryItem = withLocalState(this, 'item', { id: 'sword', name: 'Iron Sword', quantity: 1 }, {
validator: inventoryItemValidator()
});
}
}
function customEmailValidator(value: unknown): boolean | string {
const email = value as string;
if (typeof email !== 'string') {
return 'Value must be a string';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return 'Value must be a valid email address';
}
return true;
}
export class GameScene extends Phaser.Scene {
create() {
const playerEmail = withLocalState(this, 'email', 'player@game.com', {
validator: customEmailValidator
});
function updateEmail(newEmail: string) {
try {
playerEmail.set(newEmail);
console.log('Email updated successfully');
return true;
} catch (error) {
console.error('Email validation failed:', error.message);
// Handle the error appropriately
this.showErrorMessage('Please enter a valid email address');
return false;
}
}
}
}
// ✅ Good - Clear error message
function goodValidator(value: unknown): boolean | string {
if (typeof value !== 'string') {
return 'Value must be a string';
}
return true;
}
// ❌ Bad - Unclear error message
function badValidator(value: unknown): boolean | string {
if (typeof value !== 'string') {
return 'Invalid';
}
return true;
}
// ✅ Good - Type-safe validation
function typeSafeValidator(value: unknown): boolean | string {
const obj = value as { id: string; name: string };
if (typeof obj.id !== 'string') {
return 'ID must be a string';
}
if (typeof obj.name !== 'string') {
return 'Name must be a string';
}
return true;
}
// ✅ Good - Reusable validator factory
function createRangeValidator(min: number, max: number) {
return (value: unknown): boolean | string => {
const num = value as number;
if (typeof num !== 'number') {
return 'Value must be a number';
}
if (num < min || num > max) {
return `Value must be between ${min} and ${max}`;
}
return true;
};
}
// Usage
const healthValidator = createRangeValidator(0, 100);
const scoreValidator = createRangeValidator(0, 999999);
function urlValidator(value: unknown): boolean | string {
const url = value as string;
if (typeof url !== 'string') {
return 'Value must be a string';
}
try {
new URL(url);
return true;
} catch {
return 'Value must be a valid URL';
}
}
function dateValidator(value: unknown): boolean | string {
const date = value as string;
if (typeof date !== 'string') {
return 'Value must be a string';
}
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
return 'Value must be a valid date';
}
return true;
}
function passwordValidator(minLength: number = 8) {
return (value: unknown): boolean | string => {
const password = value as string;
if (typeof password !== 'string') {
return 'Value must be a string';
}
if (password.length < minLength) {
return `Password must be at least ${minLength} characters long`;
}
if (!/[A-Z]/.test(password)) {
return 'Password must contain at least one uppercase letter';
}
if (!/[a-z]/.test(password)) {
return 'Password must contain at least one lowercase letter';
}
if (!/\d/.test(password)) {
return 'Password must contain at least one number';
}
return true;
};
}