Skip to content

Validators Reference

Validators ensure state values meet specific criteria before being set. Phaser Hooks provides built-in validators for common patterns and supports custom validation functions.

type Validator<T> = (value: T) => true | string;
  • Return true if the value is valid
  • Return a string with an error message if invalid

Validates that a number falls within a specified range.

validators.numberRange(min: number, max: number): Validator<number>
ParameterTypeDescription
minnumberMinimum allowed value (inclusive)
maxnumberMaximum allowed value (inclusive)

Validator<number> - Validator function

import { withLocalState, validators } from 'phaser-hooks';
const scoreState = withLocalState(this, 'score', 0, {
validator: validators.numberRange(0, 1000),
});
scoreState.set(500); // Valid
scoreState.set(1000); // Valid
try {
scoreState.set(1500); // Throws error
} catch (error) {
console.error(error.message); // "Value must be between 0 and 1000"
}
try {
scoreState.set(-10); // Throws error
} catch (error) {
console.error(error.message); // "Value must be between 0 and 1000"
}
  • Score limits
  • Health/mana values
  • Volume controls (0-1)
  • Level ranges

Validates that a string is not empty (after trimming whitespace).

validators.nonEmptyString: Validator<string>
import { withLocalState, validators } from 'phaser-hooks';
const nameState = withLocalState(this, 'name', '', {
validator: validators.nonEmptyString,
});
nameState.set('Player1'); // Valid
nameState.set(' John '); // Valid
try {
nameState.set(''); // Throws error
} catch (error) {
console.error(error.message); // "Value cannot be an empty string"
}
try {
nameState.set(' '); // Throws error (whitespace only)
} catch (error) {
console.error(error.message); // "Value cannot be an empty string"
}
  • Player names
  • Input fields
  • Required text fields
  • Search queries

Validates that an array’s length falls within a specified range.

validators.arrayLength(min: number, max?: number): Validator<any[]>
ParameterTypeDescription
minnumberMinimum array length (inclusive)
maxnumber (optional)Maximum array length (inclusive). Omit for no upper limit.

Validator<any[]> - Validator function

import { withLocalState, validators } from 'phaser-hooks';
// Inventory with 2-4 items
const inventoryState = withLocalState<string[]>(this, 'inventory', [], {
validator: validators.arrayLength(2, 4),
});
inventoryState.set(['sword', 'potion']); // Valid (2 items)
inventoryState.set(['sword', 'potion', 'shield']); // Valid (3 items)
inventoryState.set(['sword', 'potion', 'shield', 'armor']); // Valid (4 items)
try {
inventoryState.set(['sword']); // Throws error (only 1 item)
} catch (error) {
console.error(error.message); // "Array length must be between 2 and 4"
}
try {
// Throws error (5 items)
inventoryState.set(['sword', 'potion', 'shield', 'armor', 'ring']);
} catch (error) {
console.error(error.message); // "Array length must be between 2 and 4"
}
// No maximum limit
const listState = withLocalState<number[]>(this, 'list', [], {
validator: validators.arrayLength(1), // At least 1 item, no max
});
listState.set([1]); // Valid
listState.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // Valid (no max limit)
  • Inventory systems
  • Party member limits
  • Skill slot restrictions
  • Queue management

Validates that a value is one of the allowed values.

validators.oneOf<T>(allowedValues: T[]): Validator<T>
ParameterTypeDescription
allowedValuesT[]Array of allowed values

Validator<T> - Validator function

import { withGlobalState, validators } from 'phaser-hooks';
type Difficulty = 'easy' | 'normal' | 'hard';
const difficultyState = withGlobalState<Difficulty>(
this,
'difficulty',
'normal',
{
validator: validators.oneOf(['easy', 'normal', 'hard']),
}
);
difficultyState.set('easy'); // Valid
difficultyState.set('hard'); // Valid
try {
difficultyState.set('extreme' as Difficulty); // Throws error
} catch (error) {
console.error(error.message); // "Value must be one of: easy, normal, hard"
}
// With other types
const qualityState = withLocalState<number>(this, 'quality', 1, {
validator: validators.oneOf([1, 2, 3, 4, 5]),
});
qualityState.set(3); // Valid
try {
qualityState.set(10); // Throws error
} catch (error) {
console.error(error.message); // "Value must be one of: 1, 2, 3, 4, 5"
}
  • Difficulty levels
  • Quality settings
  • Game modes
  • Character classes
  • Enum-like values

const healthValidator = (value: number): true | string => {
if (value < 0) {
return 'Health cannot be negative';
}
if (value > 100) {
return 'Health cannot exceed 100';
}
return true;
};
const healthState = withLocalState(this, 'health', 100, {
validator: healthValidator,
});
interface PlayerData {
name: string;
level: number;
inventory: string[];
}
const playerValidator = (value: PlayerData): true | string => {
// Validate name
if (!value.name || value.name.trim().length < 3) {
return 'Player name must be at least 3 characters';
}
// Validate level
if (value.level < 1 || value.level > 100) {
return 'Player level must be between 1 and 100';
}
// Validate inventory
if (value.inventory.length > 20) {
return 'Inventory cannot exceed 20 items';
}
// Check for duplicate items
const uniqueItems = new Set(value.inventory);
if (uniqueItems.size !== value.inventory.length) {
return 'Inventory cannot contain duplicate items';
}
return true;
};
const playerState = withLocalState<PlayerData>(
this,
'player',
{
name: 'Player',
level: 1,
inventory: [],
},
{ validator: playerValidator }
);
const emailValidator = (value: string): true | string => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
return 'Invalid email address format';
}
return true;
};
const emailState = withLocalState(this, 'email', '', {
validator: emailValidator,
});
const usernameValidator = (value: string): true | string => {
// Check length
if (value.length < 3 || value.length > 20) {
return 'Username must be between 3 and 20 characters';
}
// Check characters
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
return 'Username can only contain letters, numbers, and underscores';
}
// Cannot start with number
if (/^[0-9]/.test(value)) {
return 'Username cannot start with a number';
}
return true;
};
const usernameState = withLocalState(this, 'username', '', {
validator: usernameValidator,
});
const createRangeValidator = (min: number, max: number, fieldName: string) => {
return (value: number): true | string => {
if (value < min || value > max) {
return `${fieldName} must be between ${min} and ${max}`;
}
return true;
};
};
const ageState = withLocalState(this, 'age', 18, {
validator: createRangeValidator(13, 120, 'Age'),
});

Combine multiple validators:

const composeValidators = <T>(...validators: Validator<T>[]): Validator<T> => {
return (value: T): true | string => {
for (const validator of validators) {
const result = validator(value);
if (result !== true) {
return result; // Return first error
}
}
return true;
};
};
// Usage
const usernameState = withLocalState(this, 'username', '', {
validator: composeValidators(
validators.nonEmptyString,
(value: string) => {
if (value.length < 3) return 'Username too short';
return true;
},
(value: string) => {
if (!/^[a-zA-Z]/.test(value)) return 'Username must start with a letter';
return true;
}
),
});

Apply different validation based on context:

export class GameScene extends Phaser.Scene {
private isDeveloperMode = false;
create() {
const scoreValidator = (value: number): true | string => {
// Skip validation in developer mode
if (this.isDeveloperMode) {
return true;
}
// Normal validation
if (value < 0 || value > 999999) {
return 'Score must be between 0 and 999999';
}
return true;
};
const scoreState = withLocalState(this, 'score', 0, {
validator: scoreValidator,
});
}
}

Validators should not have side effects:

// ❌ Bad - has side effects
const badValidator = (value: number): true | string => {
console.log('Validating...'); // Side effect
this.someProperty = value; // Side effect
return value >= 0 ? true : 'Invalid';
};
// ✅ Good - pure function
const goodValidator = (value: number): true | string => {
return value >= 0 ? true : 'Invalid';
};
// ❌ Bad - vague error
const badValidator = (value: string): true | string => {
return value.length >= 3 ? true : 'Invalid';
};
// ✅ Good - descriptive error
const goodValidator = (value: string): true | string => {
return value.length >= 3 ? true : 'Username must be at least 3 characters';
};

Catch errors before they cause problems:

const playerState = withLocalState(
this,
'player',
{ hp: 100 },
{
validator: (player) => {
if (player.hp < 0) return 'HP cannot be negative';
return true;
},
}
);
try {
playerState.set({ hp: -10 });
} catch (error) {
// Handle error immediately
console.error('Invalid player state:', error.message);
this.showErrorToUser(error.message);
}
// Test validator with various inputs
const validator = validators.numberRange(0, 100);
console.assert(validator(50) === true);
console.assert(validator(0) === true);
console.assert(validator(100) === true);
console.assert(typeof validator(-1) === 'string');
console.assert(typeof validator(101) === 'string');