TypeScript Property Decorator Challenge: Data Validation
Decorators are a powerful feature in TypeScript that allow you to annotate and modify classes, methods, properties, and parameters. This challenge will focus on creating property decorators to implement declarative data validation, a common and useful pattern in application development. By using decorators, you can keep your validation logic separate from your core business logic, making your code cleaner and more maintainable.
Problem Description
Your task is to create a set of TypeScript property decorators that can be applied to class properties to enforce validation rules. These decorators should automatically check the validity of a property's value when it's assigned. If the value is invalid, an error should be thrown.
Key Requirements:
@RequiredDecorator: This decorator should ensure that a property is not assignednullorundefined.@ValidateStringDecorator: This decorator should take optionalminLengthandmaxLengtharguments and validate that the property is a string within the specified length constraints. IfminLengthis provided, it must be greater than or equal to 0. IfmaxLengthis provided, it must be greater thanminLength.- Chaining Decorators: You should be able to chain multiple property decorators on a single property. The validation should occur in the order the decorators are defined (from top to bottom).
- Error Handling: When validation fails, a descriptive
Errorshould be thrown, indicating which property failed and why. - Decorator Factory: For decorators that accept arguments (
@ValidateString), you will need to implement them as decorator factories.
Expected Behavior:
When a property decorated with @Required or @ValidateString is assigned a value:
- If the value passes all applicable validations, the assignment proceeds normally.
- If the value fails any validation, an
Erroris thrown immediately.
Edge Cases:
- What happens if
@ValidateStringis called withoutminLengthormaxLength? - What happens if
@Requiredis applied to a property that is already explicitly typed asnullorundefined? (Consider how TypeScript typing interacts with runtime validation).
Examples
Example 1: Basic @Required
class User {
@Required
username: string;
@Required
age: number;
constructor(username: string, age: number) {
this.username = username;
this.age = age;
}
}
// Valid assignment
const user1 = new User("alice", 30);
console.log("User 1 created successfully.");
// Invalid assignment (throws error)
try {
const user2 = new User(null as any, 25); // Intentionally passing null
} catch (error: any) {
console.error(error.message); // Expected: "Property 'username' is required."
}
try {
const user3 = new User("bob", undefined as any); // Intentionally passing undefined
} catch (error: any) {
console.error(error.message); // Expected: "Property 'age' is required."
}
Output:
User 1 created successfully.
Property 'username' is required.
Property 'age' is required.
Example 2: @ValidateString with minLength and maxLength
class Product {
@ValidateString({ minLength: 3, maxLength: 50 })
name: string;
constructor(name: string) {
this.name = name;
}
}
// Valid assignments
const product1 = new Product("Laptop");
console.log("Product 1 created successfully.");
const product2 = new Product("A very long product name that fits within 50 characters.");
console.log("Product 2 created successfully.");
// Invalid assignments (throws errors)
try {
const product3 = new Product("AB"); // Too short
} catch (error: any) {
console.error(error.message); // Expected: "Property 'name' must be a string with a minimum length of 3 and a maximum length of 50."
}
try {
const product4 = new Product("This product name is significantly longer than the allowed fifty characters and should trigger an error. It's way too long."); // Too long
} catch (error: any) {
console.error(error.message); // Expected: "Property 'name' must be a string with a minimum length of 3 and a maximum length of 50."
}
try {
const product5 = new Product(null as any); // Not a string
} catch (error: any) {
console.error(error.message); // Expected: "Property 'name' must be a string with a minimum length of 3 and a maximum length of 50."
}
Output:
Product 1 created successfully.
Product 2 created successfully.
Property 'name' must be a string with a minimum length of 3 and a maximum length of 50.
Property 'name' must be a string with a minimum length of 3 and a maximum length of 50.
Property 'name' must be a string with a minimum length of 3 and a maximum length of 50.
Example 3: Chaining Decorators
class Config {
@Required
@ValidateString({ minLength: 1 })
settingName: string;
@ValidateString({ maxLength: 255 })
description: string | null = null; // Optional property with default value
constructor(settingName: string) {
this.settingName = settingName;
}
}
// Valid assignments
const config1 = new Config("api_key");
console.log("Config 1 created successfully.");
config1.description = "This is a valid description.";
console.log("Config 1 description updated successfully.");
// Invalid assignments (throws errors)
try {
const config2 = new Config(""); // Fails @ValidateString
} catch (error: any) {
console.error(error.message); // Expected: "Property 'settingName' must be a string with a minimum length of 1 and a maximum length of undefined."
}
try {
const config3 = new Config("timeout");
config3.description = "Short"; // Valid
config3.description = "This is a very long description for the configuration setting, exceeding the maximum allowed length of two hundred and fifty-five characters by a significant margin. It should trigger an error when assigned."; // Fails @ValidateString on description
} catch (error: any) {
console.error(error.message); // Expected: "Property 'description' must be a string with a minimum length of undefined and a maximum length of 255."
}
try {
const config4 = new Config(undefined as any); // Fails @Required
} catch (error: any) {
console.error(error.message); // Expected: "Property 'settingName' is required."
}
Output:
Config 1 created successfully.
Config 1 description updated successfully.
Property 'settingName' must be a string with a minimum length of 1 and a maximum length of undefined.
Property 'description' must be a string with a minimum length of undefined and a maximum length of 255.
Property 'settingName' is required.
Constraints
- The decorators should work for properties of any type, but
@ValidateStringshould only validate if the assigned value is actually a string. - The validation should be performed at runtime, specifically when a property is assigned a value (e.g., during object construction or via direct property assignment).
- Your solution should be implemented using TypeScript's experimental decorator support, enabled by setting
"experimentalDecorators": trueand"emitDecoratorMetadata": true(though metadata isn't strictly required for this challenge, it's good practice to include) in yourtsconfig.json. - The validation logic should be contained within the decorators themselves. Avoid adding explicit validation checks within the class methods or constructors.
Notes
- Remember that property decorators in TypeScript are executed when the class is defined, not when an instance is created. To intercept property assignments, you'll need to use a technique that allows you to hook into the assignment process. This typically involves overriding the property setter.
- You'll need to define the
ValidateStringOptionsinterface for the@ValidateStringdecorator's configuration. - Consider how you will access the property name and the target object within the decorator implementation. The
propertyKeyparameter in a property decorator is the name of the property. - The use of
Object.definePropertywill be crucial for replacing the original property with one that includes validation logic in its setter. - Think about how to handle cases where a property might not have a setter initially (e.g., if it's declared without an initial value).