TypeScriptでデコレーターを使った型安全なバリデーション実装方法

TypeScriptは静的型付け言語として、型安全なコードを書くことができる強力なツールです。しかし、入力値が正しいかどうかを保証するためには、型情報だけでは十分でないことがあります。ここで役立つのが「バリデーション」です。特に、デコレーターを使用すると、コードの可読性を保ちながら、簡潔にバリデーションロジックを実装できます。

本記事では、TypeScriptでデコレーターを使用して、型安全な入力チェックを実装する方法を詳しく解説します。デコレーターの基本概念から、カスタムバリデーションロジックの作成、そして応用例までを網羅し、実用的な知識を提供します。

目次

デコレーターの基本概念

デコレーターとは、TypeScriptやJavaScriptにおいてクラスやそのメンバー(プロパティ、メソッド)に付加情報を追加するための特殊な構文です。デコレーターを使用すると、対象となるクラスやメソッドの振る舞いを変更したり、追加の機能を提供することができます。

デコレーターの仕組み

デコレーターは、関数として実装され、クラスやメソッド、プロパティに対して修飾を行います。デコレーターが適用されると、その対象となるコードが実行される前に、デコレーターが介入して特定の処理を挟むことができます。例えば、メソッドが呼び出される前にログを記録する、引数を検証する、あるいはプロパティの値を強制的に変更するといった動作が可能です。

デコレーターの基本的な使い方

TypeScriptでは、クラスやメソッドの上に@decoratorNameという形でデコレーターを適用します。以下は基本的なデコレーターの例です。

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyKey} was called with arguments: ${args}`);
        return originalMethod.apply(this, args);
    };
}

class ExampleClass {
    @Log
    sayHello(name: string) {
        console.log(`Hello, ${name}`);
    }
}

const example = new ExampleClass();
example.sayHello('John');

この例では、LogデコレーターがsayHelloメソッドに適用され、メソッドの呼び出し時に引数がログに記録されます。

デコレーターは、コードに付加的な機能を簡潔に追加できる便利な機能であり、特にバリデーションのようなクロスカッティングな処理に有効です。次の章では、具体的にバリデーションにデコレーターを活用する方法について解説します。

バリデーションの必要性

ソフトウェア開発において、バリデーションは非常に重要な役割を果たします。バリデーションとは、入力データが期待される形式や範囲に収まっているかを確認し、異常なデータをシステムに流れ込むのを防ぐための手段です。

入力データの信頼性確保

ユーザーや外部のシステムから受け取るデータは、常に正しい形式や値であるとは限りません。バリデーションを行うことで、以下のような問題を防ぐことができます。

  • 不正なデータの処理:誤ったデータが処理されると、システム全体に悪影響を与える可能性があります。特に数値や日付など、正確なフォーマットが求められる場合、適切なバリデーションが必要です。
  • セキュリティの強化:悪意のある入力(例: SQLインジェクションやクロスサイトスクリプティング)を防ぐためにも、バリデーションは重要です。

バグの防止とユーザーエクスペリエンス向上

バリデーションが適切に行われていないと、エラーの原因が曖昧になり、バグの発生源を特定しにくくなります。逆に、バリデーションをしっかりと設けておけば、ユーザーはどのデータが不正なのかすぐに理解でき、修正の手間が軽減され、ユーザーエクスペリエンスが向上します。

型安全とバリデーションの併用

TypeScriptは型システムを活用して、開発者がコード内でデータの型を保証しますが、実行時のデータチェックは行われません。ここでバリデーションを加えることで、型安全だけでなく、実際のデータに対する厳密なチェックも実現できます。特にデコレーターを活用することで、バリデーションを簡単かつ効果的にコードベースに組み込むことが可能です。

次の章では、TypeScriptにおけるデコレーターの種類と、それぞれがどのように使えるかについて解説します。

TypeScriptにおけるデコレーターの種類

TypeScriptでは、デコレーターはさまざまな場面で使用することができ、主に以下の4つの種類に分類されます。それぞれのデコレーターは異なる要素に適用でき、特定の役割を持ちます。ここでは、それぞれの種類について説明します。

クラスデコレーター

クラスデコレーターは、クラス全体に適用され、そのクラスに対して何らかの追加の振る舞いを付与したり、クラスを拡張するために使用されます。クラスデコレーターは、クラスの定義を受け取って処理を行い、クラスそのものを変更することも可能です。

function Seal(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@Seal
class ExampleClass {
    property: string;
    constructor(property: string) {
        this.property = property;
    }
}

この例では、Sealデコレーターを使ってクラスとそのプロトタイプをシール(変更不能)にしています。

メソッドデコレーター

メソッドデコレーターは、特定のメソッドに適用され、メソッドの動作を変更したり、メソッドの呼び出し時に追加の処理を行うことができます。たとえば、メソッドの引数や戻り値を検証したり、ログを取る際に使われます。

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyKey} was called with arguments: ${args}`);
        return originalMethod.apply(this, args);
    };
}

このように、Logデコレーターをメソッドに適用すると、メソッドが呼び出されるたびに引数がログに記録されます。

プロパティデコレーター

プロパティデコレーターは、クラスのプロパティに対して使用されます。主にプロパティの変更を監視したり、特定の条件でプロパティの値を制約するために使われます。

function Readonly(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
        writable: false
    });
}

class ExampleClass {
    @Readonly
    name: string = 'John';
}

この例では、Readonlyデコレーターを使って、nameプロパティを変更不可能にしています。

パラメータデコレーター

パラメータデコレーターは、メソッドのパラメータに適用され、メソッドの引数に対して処理を行います。たとえば、パラメータの型チェックや前処理を行う場合に使用されます。

function Validate(target: any, propertyKey: string, parameterIndex: number) {
    console.log(`Parameter in position ${parameterIndex} at method ${propertyKey} is being validated`);
}

class ExampleClass {
    greet(@Validate name: string) {
        console.log(`Hello, ${name}`);
    }
}

この例では、Validateデコレーターが、メソッドの引数に対してバリデーションの処理を追加しています。

次の章では、具体的にクラスデコレーターを使ってバリデーションを実装する方法について説明します。

クラスデコレーターでバリデーションを行う方法

クラスデコレーターは、クラス全体に対してバリデーションや特定の機能を付加するために使用できます。クラスデコレーターを用いることで、クラスのプロパティやメソッドに共通のバリデーションロジックを一括で適用することが可能です。

ここでは、クラスデコレーターを使って、入力データのバリデーションを実装する方法を紹介します。

クラスデコレーターの基本的なバリデーション

まず、クラスデコレーターを利用して、クラスのプロパティに対するバリデーションを行う例を見てみましょう。以下のコードでは、プロパティに対して「必須項目」であることをチェックするバリデーションを実装します。

function Required(target: any) {
    return class extends target {
        constructor(...args: any[]) {
            super(...args);
            for (const key in this) {
                if (this[key] === undefined || this[key] === null) {
                    throw new Error(`Property ${key} is required`);
                }
            }
        }
    };
}

@Required
class User {
    name: string;
    email: string;

    constructor(name: string, email: string) {
        this.name = name;
        this.email = email;
    }
}

try {
    const user = new User('John Doe', ''); // emailが空のためエラーを投げる
} catch (e) {
    console.error(e.message);  // "Property email is required"
}

この例では、Requiredというクラスデコレーターが、クラス内のプロパティを検査し、値がnullまたはundefinedの場合にエラーを投げるバリデーションを行っています。デコレーターが適用されたクラスのインスタンスを生成する際に、すべてのプロパティが適切に設定されていることを確認します。

クラスデコレーターの拡張:複数のバリデーションを組み合わせる

クラスデコレーターは、複数のバリデーションロジックを組み合わせて実装することも可能です。例えば、次のコードでは、プロパティが必須であることに加えて、文字列が一定の長さ以上であることもチェックするバリデーションを作成します。

function MinLength(length: number) {
    return function(target: any, propertyKey: string) {
        let value = target[propertyKey];

        const getter = () => value;
        const setter = (newValue: string) => {
            if (newValue.length < length) {
                throw new Error(`Property ${propertyKey} should have at least ${length} characters`);
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

@Required
class Product {
    @MinLength(5)
    title: string;

    constructor(title: string) {
        this.title = title;
    }
}

try {
    const product = new Product('Pen'); // タイトルが短いためエラーを投げる
} catch (e) {
    console.error(e.message);  // "Property title should have at least 5 characters"
}

この例では、MinLengthというプロパティデコレーターを使って、titleプロパティが少なくとも5文字以上であることをチェックしています。Requiredクラスデコレーターとの組み合わせで、より強力なバリデーションを実現しています。

クラスデコレーターの利点

クラスデコレーターを使うことで、次のような利点があります。

  • コードの再利用性:バリデーションロジックを一箇所にまとめて再利用できるため、重複コードが減ります。
  • 一貫性のあるバリデーション:クラス単位で統一されたバリデーションを適用できるため、システム全体で一貫した入力チェックが実現します。
  • メンテナンスが容易:デコレーターを使ってバリデーションを一元管理することで、メンテナンス性が向上し、新しいバリデーションルールを簡単に追加できます。

次の章では、プロパティデコレーターを使用したバリデーションの応用例について解説します。

プロパティデコレーターの応用

プロパティデコレーターは、クラスのプロパティに直接バリデーションや特定の制約を付加するために使用されます。プロパティの変更や読み取り時にバリデーションを適用することで、個別のプロパティに対して詳細なチェックを行うことが可能です。

ここでは、プロパティデコレーターを使ったバリデーションの応用例を見ていきます。

プロパティデコレーターの基本的な仕組み

プロパティデコレーターは、クラスのプロパティが変更される際に特定のロジックを挿入することができます。たとえば、プロパティに割り当てられる値が特定の条件を満たしているかを確認し、条件に反する場合はエラーを発生させるといったことが可能です。

function IsPositive(target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;
    const setter = (newValue: number) => {
        if (newValue <= 0) {
            throw new Error(`Property ${propertyKey} must be a positive number`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class Product {
    @IsPositive
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

try {
    const product = new Product(-10);  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "Property price must be a positive number"
}

この例では、@IsPositiveというプロパティデコレーターを使って、priceプロパティが正の値であることを強制しています。価格が0以下の値を持つ場合、エラーメッセージが表示されます。

複数のバリデーションロジックを組み合わせる

プロパティデコレーターを複数適用することで、複数のバリデーションロジックを組み合わせることができます。例えば、プロパティが正の値であることに加え、数値が特定の範囲内に収まっているかどうかもチェックしたい場合、次のようにデコレーターを組み合わせます。

function IsInRange(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: number;

        const getter = () => value;
        const setter = (newValue: number) => {
            if (newValue < min || newValue > max) {
                throw new Error(`Property ${propertyKey} must be between ${min} and ${max}`);
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

class Product {
    @IsPositive
    @IsInRange(1, 100)
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

try {
    const product = new Product(150);  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "Property price must be between 1 and 100"
}

この例では、@IsPositive@IsInRange(1, 100)の両方のデコレーターがpriceプロパティに適用されています。priceが正の値かつ1から100の範囲内であることを強制します。150の値が入力されたため、範囲外エラーが発生します。

カスタムバリデーションメッセージの追加

バリデーションエラーに対して、より詳細なエラーメッセージをカスタマイズすることも可能です。プロパティデコレーターを応用すれば、デフォルトのエラーメッセージではなく、ユーザーにとって分かりやすいメッセージを指定することができます。

function IsEmail(target: any, propertyKey: string) {
    let value: string;

    const getter = () => value;
    const setter = (newValue: string) => {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(newValue)) {
            throw new Error(`${propertyKey} must be a valid email address`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class User {
    @IsEmail
    email: string;

    constructor(email: string) {
        this.email = email;
    }
}

try {
    const user = new User('invalid-email');  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "email must be a valid email address"
}

この例では、@IsEmailデコレーターを使用して、emailプロパティが正しいメールアドレス形式であることを確認しています。無効なメールアドレスが入力された場合、具体的なエラーメッセージをユーザーに表示します。

プロパティデコレーターを活用することで、個々のプロパティに対する細かなバリデーションが簡単に実装でき、入力チェックの精度を大幅に向上させることができます。

次の章では、メソッドデコレーターを使ったバリデーションの実装方法について解説します。

メソッドデコレーターで入力の検証

メソッドデコレーターは、メソッドの呼び出し時にその動作を修正したり、引数に対してバリデーションを行うために使用されます。これにより、特定の条件下でメソッドの実行を制御したり、メソッドが期待される形式で呼び出されたかどうかを検証することが可能です。

ここでは、メソッドデコレーターを使って入力値のバリデーションを行う具体的な方法を紹介します。

メソッドデコレーターの基本的な使い方

メソッドデコレーターを使用して、メソッドの引数が正しい値であるかを確認するバリデーションを行うことができます。以下の例では、数値が負の値でないことを確認するバリデーションをメソッドに適用します。

function ValidatePositive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        if (args[0] <= 0) {
            throw new Error(`Argument for ${propertyKey} must be a positive number`);
        }
        return originalMethod.apply(this, args);
    };
}

class Calculator {
    @ValidatePositive
    calculateSquareRoot(value: number) {
        return Math.sqrt(value);
    }
}

try {
    const calculator = new Calculator();
    console.log(calculator.calculateSquareRoot(-9));  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "Argument for calculateSquareRoot must be a positive number"
}

この例では、@ValidatePositiveというメソッドデコレーターを使用して、calculateSquareRootメソッドに渡される引数が正の値であることを確認しています。もし負の数が入力された場合、エラーメッセージが表示されます。

複数の引数に対するバリデーション

メソッドデコレーターは複数の引数に対してもバリデーションを行うことができます。次の例では、メソッドに渡される2つの数値がいずれも正の値であるかどうかを確認します。

function ValidateAllPositive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        for (let arg of args) {
            if (arg <= 0) {
                throw new Error(`All arguments for ${propertyKey} must be positive numbers`);
            }
        }
        return originalMethod.apply(this, args);
    };
}

class Calculator {
    @ValidateAllPositive
    multiply(a: number, b: number) {
        return a * b;
    }
}

try {
    const calculator = new Calculator();
    console.log(calculator.multiply(3, -4));  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "All arguments for multiply must be positive numbers"
}

この例では、@ValidateAllPositiveデコレーターを使って、メソッドmultiplyに渡されるすべての引数が正の値であることを確認しています。負の数が含まれていると、エラーメッセージが表示されます。

非同期メソッドのバリデーション

非同期メソッドにもメソッドデコレーターを適用し、引数のバリデーションを行うことが可能です。以下の例では、非同期メソッドに対して引数が正の値であることを確認しつつ、エラーハンドリングを行っています。

function ValidateAsyncPositive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
        if (args[0] <= 0) {
            throw new Error(`Argument for ${propertyKey} must be a positive number`);
        }
        return await originalMethod.apply(this, args);
    };
}

class Calculator {
    @ValidateAsyncPositive
    async calculateAsyncSquareRoot(value: number) {
        return new Promise<number>((resolve) => {
            setTimeout(() => resolve(Math.sqrt(value)), 1000);
        });
    }
}

(async () => {
    const calculator = new Calculator();
    try {
        console.log(await calculator.calculateAsyncSquareRoot(-9));  // エラーが発生する
    } catch (e) {
        console.error(e.message);  // "Argument for calculateAsyncSquareRoot must be a positive number"
    }
})();

この例では、非同期メソッドcalculateAsyncSquareRootに対してバリデーションを適用しています。非同期メソッドでも同様にバリデーションを行い、結果を返す前に引数の値を確認することができます。

複数のメソッドデコレーターの適用

メソッドデコレーターを複数組み合わせることも可能です。例えば、引数が正しい範囲内にあるかどうかをチェックしつつ、メソッド呼び出し前にログを記録するようなデコレーターを組み合わせることができます。

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey} with arguments: ${args}`);
        return originalMethod.apply(this, args);
    };
}

@ValidateAllPositive
@LogMethod
function multiply(a: number, b: number) {
    return a * b;
}

このように、複数のメソッドデコレーターを組み合わせることで、メソッドに対するさまざまな処理を一度に適用することができます。

次の章では、カスタムバリデーションロジックを実装する方法について詳しく解説します。

カスタムバリデーションロジックの実装方法

デコレーターを使用してバリデーションを実装する際、既存のデコレーターだけでなく、自分でカスタムバリデーションロジックを作成することが可能です。カスタムデコレーターを作ることで、プロジェクト固有のルールや要件に基づいたバリデーションを実現できます。

ここでは、カスタムバリデーションロジックをデコレーターとして実装する方法を解説します。

カスタムバリデーションの基本

カスタムバリデーションデコレーターは、特定の要件を満たすようにロジックを自由に設計できます。以下の例では、文字列の長さが指定した範囲内に収まっているかをチェックするカスタムデコレーターを作成します。

function Length(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: string;

        const getter = () => value;
        const setter = (newValue: string) => {
            if (newValue.length < min || newValue.length > max) {
                throw new Error(`${propertyKey} must be between ${min} and ${max} characters long`);
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

class User {
    @Length(3, 15)
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

try {
    const user = new User('Al');  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "username must be between 3 and 15 characters long"
}

この例では、@Length(3, 15)というデコレーターをusernameプロパティに適用し、3文字以上15文字以下であることをバリデーションしています。2文字のユーザー名を入力するとエラーが発生します。

複雑なバリデーションロジックの実装

より複雑なバリデーションロジックもカスタムデコレーターで実装可能です。例えば、メールアドレスや電話番号といった形式に従う必要がある入力に対して、正規表現を使ったバリデーションを追加できます。

function IsEmail(target: any, propertyKey: string) {
    let value: string;

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    const getter = () => value;
    const setter = (newValue: string) => {
        if (!emailRegex.test(newValue)) {
            throw new Error(`${propertyKey} must be a valid email address`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class User {
    @IsEmail
    email: string;

    constructor(email: string) {
        this.email = email;
    }
}

try {
    const user = new User('invalid-email');  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "email must be a valid email address"
}

この例では、@IsEmailデコレーターを使って、emailプロパティが正しいメールアドレス形式であるかをバリデーションしています。不正なメールアドレス形式の場合、カスタムエラーメッセージを表示します。

条件付きバリデーションのカスタム実装

さらに複雑なバリデーション要件では、条件に基づいたカスタムバリデーションを作成することが有効です。例えば、ある条件が満たされている場合のみ特定のバリデーションを実行するというロジックを実装できます。

function ConditionalValidation(condition: (target: any) => boolean) {
    return function (target: any, propertyKey: string) {
        let value: any;

        const getter = () => value;
        const setter = (newValue: any) => {
            if (condition(target)) {
                if (newValue === undefined || newValue === null) {
                    throw new Error(`${propertyKey} is required under the current condition`);
                }
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

class UserProfile {
    isPremiumUser: boolean;

    @ConditionalValidation((target) => target.isPremiumUser)
    creditCardInfo?: string;

    constructor(isPremiumUser: boolean, creditCardInfo?: string) {
        this.isPremiumUser = isPremiumUser;
        this.creditCardInfo = creditCardInfo;
    }
}

try {
    const userProfile = new UserProfile(true);  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "creditCardInfo is required under the current condition"
}

この例では、@ConditionalValidationデコレーターを使って、isPremiumUserプロパティがtrueの場合にのみ、creditCardInfoが必須であることを確認しています。条件に基づいてバリデーションの適用範囲を動的に制御することができます。

複数のカスタムデコレーターを組み合わせる

カスタムデコレーターを複数組み合わせて使用することもできます。例えば、以下のように@Length@IsEmailを組み合わせて、入力が一定の長さであり、かつ正しいメールアドレス形式であるかを同時にチェックすることが可能です。

class User {
    @Length(5, 20)
    @IsEmail
    email: string;

    constructor(email: string) {
        this.email = email;
    }
}

try {
    const user = new User('abc@xyz');  // 長さの条件を満たさないためエラーが発生する
} catch (e) {
    console.error(e.message);
}

この例では、@Length(5, 20)@IsEmailの両方のデコレーターをemailプロパティに適用し、メールアドレスの形式と長さの両方をチェックしています。

カスタムバリデーションロジックをデコレーターとして実装することで、アプリケーションの要件に柔軟に対応でき、より堅牢なバリデーションを行うことができます。

次の章では、型安全を担保しつつバリデーションを行う際の重要なポイントについて解説します。

型安全を担保するポイント

TypeScriptは静的型付け言語であり、型安全なコードを記述することが可能ですが、バリデーションを実装する際には、型安全を維持しつつ柔軟な入力チェックを行うことが重要です。ここでは、バリデーション実装時に型安全を損なわずに進めるための重要なポイントを解説します。

型アノテーションの徹底

TypeScriptの利点の一つは、型を明示的に指定することで、コンパイル時にエラーを検出できる点です。バリデーションを実装する際、引数やプロパティに対してしっかりと型を定義することで、型エラーを未然に防ぐことができます。これにより、コードの安全性が向上し、予期しないエラーが発生しにくくなります。

class User {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

型をしっかりと定義することで、実行時のエラーを減らすだけでなく、IDEによる補完機能が強化され、開発の効率が向上します。

ジェネリック型を活用したバリデーション

ジェネリック型を利用することで、柔軟かつ型安全なバリデーションを実装することができます。ジェネリック型は、異なるデータ型に対して同じ処理を適用する際に役立ちます。以下の例では、異なる型に対応するカスタムバリデーションをジェネリックを用いて実装します。

function ValidateLength<T extends { length: number }>(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: T;

        const getter = () => value;
        const setter = (newValue: T) => {
            if (newValue.length < min || newValue.length > max) {
                throw new Error(`${propertyKey} must be between ${min} and ${max} characters long`);
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

class User {
    @ValidateLength(5, 10)
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

この例では、ジェネリック型Tを使用して、lengthプロパティを持つすべての型に対応するバリデーションを実装しています。これにより、文字列や配列など、異なる型に対しても同じバリデーションを適用できます。

型ガードの活用

バリデーション処理を行う際、TypeScriptの型ガードを活用することで、実行時にデータが正しい型であることを確認しながら処理を進めることができます。型ガードは、特定の型であるかどうかをチェックする関数であり、条件を満たす場合にのみ次の処理を実行します。

function isString(value: any): value is string {
    return typeof value === 'string';
}

function ValidateString(target: any, propertyKey: string) {
    let value: any;

    const getter = () => value;
    const setter = (newValue: any) => {
        if (!isString(newValue)) {
            throw new Error(`${propertyKey} must be a string`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class User {
    @ValidateString
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

この例では、isStringという型ガードを使って、nameプロパティが文字列であることを確認しています。型ガードを使用することで、型安全なバリデーションが実現でき、予期しない型の値が渡されるリスクを回避できます。

nullとundefinedの明示的な扱い

TypeScriptではstrictNullChecksオプションを有効にすることで、nullundefinedを扱う際の安全性を強化できます。バリデーションを実装する際は、これらの値に対する明示的な処理を行い、予期せぬエラーを防ぐことが重要です。

function ValidateNotNull(target: any, propertyKey: string) {
    let value: any;

    const getter = () => value;
    const setter = (newValue: any) => {
        if (newValue === null || newValue === undefined) {
            throw new Error(`${propertyKey} cannot be null or undefined`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class Product {
    @ValidateNotNull
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

try {
    const product = new Product(null);  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "name cannot be null or undefined"
}

この例では、@ValidateNotNullデコレーターを使って、nullundefinedが入力された場合にエラーを発生させる処理を実装しています。これにより、nullundefinedが意図しない形で扱われることを防ぎます。

ユニオン型を利用した柔軟なバリデーション

TypeScriptのユニオン型を使うことで、複数の型に対応するバリデーションを型安全に実装することが可能です。例えば、数値や文字列のどちらかを許容するプロパティに対してバリデーションを行う場合、ユニオン型を用いて柔軟に対応します。

function ValidateNumberOrString(target: any, propertyKey: string) {
    let value: string | number;

    const getter = () => value;
    const setter = (newValue: string | number) => {
        if (typeof newValue !== 'string' && typeof newValue !== 'number') {
            throw new Error(`${propertyKey} must be a string or number`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class Item {
    @ValidateNumberOrString
    identifier: string | number;

    constructor(identifier: string | number) {
        this.identifier = identifier;
    }
}

この例では、identifierプロパティが数値または文字列であることを型安全にチェックしています。これにより、さまざまなタイプのデータに対して一貫したバリデーションを行うことができます。

次の章では、デコレーターを用いたバリデーションのテスト手法について説明します。

デコレーターを用いたバリデーションのテスト手法

デコレーターを使ったバリデーションが正しく機能することを確認するためには、適切なテストが欠かせません。テストを行うことで、予期しない動作やエラーを未然に防ぐことができ、コードの信頼性が向上します。ここでは、デコレーターを利用したバリデーションのテスト手法について解説します。

ユニットテストの重要性

ユニットテストは、個々のコンポーネントや機能(この場合はバリデーションデコレーター)が期待通りに動作するかを検証するために重要です。特にデコレーターはクラスやメソッドに対して動作を追加する機能のため、その影響範囲を適切にテストする必要があります。

JavaScriptやTypeScriptでは、JestやMochaといったテストフレームワークを利用してユニットテストを実施します。ここでは、Jestを用いたテスト手法を紹介します。

Jestを使ったテストのセットアップ

まず、Jestのインストールが必要です。プロジェクトにJestを追加するには、以下のコマンドを実行します。

npm install --save-dev jest @types/jest ts-jest

次に、ts-jestを使って、TypeScriptのコードをJestで実行できるように設定します。プロジェクトのルートにjest.config.jsファイルを作成し、以下の設定を追加します。

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

これで、Jestを使ってTypeScriptコードのテストを実行する準備が整いました。

デコレーターのユニットテスト例

デコレーターでバリデーションを行うクラスに対して、ユニットテストを実施します。以下の例では、先ほど紹介した@IsPositiveデコレーターをテストします。

// positive.decorator.ts
function IsPositive(target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;
    const setter = (newValue: number) => {
        if (newValue <= 0) {
            throw new Error(`${propertyKey} must be a positive number`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class Product {
    @IsPositive
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

このProductクラスに対して、バリデーションが正しく機能するかをテストします。

// positive.decorator.test.ts
import { Product } from './positive.decorator';

describe('IsPositive Decorator', () => {
    it('should throw an error if a negative value is assigned', () => {
        expect(() => {
            new Product(-10);
        }).toThrowError('price must be a positive number');
    });

    it('should not throw an error if a positive value is assigned', () => {
        expect(() => {
            new Product(10);
        }).not.toThrow();
    });
});

このテストでは、負の値が入力された場合にエラーが発生するか、正の値が入力された場合にはエラーが発生しないかを確認しています。テストはJestのdescribeブロックを使ってグループ化され、各シナリオをitブロックで定義します。

エラーハンドリングのテスト

デコレーターでバリデーションを行う場合、エラーハンドリングが重要です。期待されるエラーメッセージが正しく出力されるかどうかもテストで確認します。

// email.decorator.test.ts
import { User } from './email.decorator';

describe('IsEmail Decorator', () => {
    it('should throw an error for an invalid email address', () => {
        expect(() => {
            new User('invalid-email');
        }).toThrowError('email must be a valid email address');
    });

    it('should not throw an error for a valid email address', () => {
        expect(() => {
            new User('test@example.com');
        }).not.toThrow();
    });
});

このテストでは、無効なメールアドレスが入力されたときに正しいエラーメッセージが出力されるかどうかを確認しています。これにより、バリデーションのエラーメッセージが意図した通りの内容かどうかを検証できます。

非同期処理を含むメソッドのテスト

非同期メソッドに対してデコレーターを適用する場合、非同期処理をテストする方法も重要です。Jestではasync/awaitを使って非同期テストを行うことができます。

// async.decorator.test.ts
import { Calculator } from './async.decorator';

describe('Async Method Decorator', () => {
    it('should throw an error for a negative value in async method', async () => {
        const calculator = new Calculator();
        await expect(calculator.calculateAsyncSquareRoot(-9)).rejects.toThrow('Argument for calculateAsyncSquareRoot must be a positive number');
    });

    it('should resolve correctly for a positive value in async method', async () => {
        const calculator = new Calculator();
        await expect(calculator.calculateAsyncSquareRoot(9)).resolves.toBe(3);
    });
});

このテストでは、非同期メソッドcalculateAsyncSquareRootが負の値に対してエラーを投げるか、正の値に対して正しく動作するかをテストしています。awaitexpect(...).rejectsexpect(...).resolvesを使うことで、非同期関数の結果を確認します。

コードカバレッジの確認

Jestは、テストカバレッジを計測する機能も提供しており、どの程度のコードがテストされているかを確認することができます。npm test -- --coverageコマンドを実行すると、カバレッジレポートが生成され、バリデーションロジックが十分にテストされているかを把握することができます。

次の章では、デコレーターを使ったバリデーションの応用例について解説します。

応用例:デコレーターでの複雑な入力チェック

ここまで、基本的なデコレーターを使ったバリデーションを解説してきましたが、実際のアプリケーションでは、より複雑な入力チェックが必要な場合があります。ここでは、デコレーターを活用して複雑なバリデーションを行う応用例を紹介します。

依存関係に基づくバリデーション

現実的なシナリオでは、あるプロパティの値が他のプロパティに依存する場合があります。例えば、ユーザーがプレミアムメンバーの場合にのみクレジットカード情報を必須にするようなケースです。このような依存関係に基づくバリデーションをデコレーターで実装することができます。

function ConditionalRequired(dependentKey: string, dependentValue: any) {
    return function (target: any, propertyKey: string) {
        let value: any;

        const getter = () => value;
        const setter = (newValue: any) => {
            if (target[dependentKey] === dependentValue && (newValue === undefined || newValue === null)) {
                throw new Error(`${propertyKey} is required when ${dependentKey} is ${dependentValue}`);
            }
            value = newValue;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
        });
    };
}

class UserProfile {
    isPremiumUser: boolean;

    @ConditionalRequired('isPremiumUser', true)
    creditCardInfo?: string;

    constructor(isPremiumUser: boolean, creditCardInfo?: string) {
        this.isPremiumUser = isPremiumUser;
        this.creditCardInfo = creditCardInfo;
    }
}

try {
    const user = new UserProfile(true);  // エラーが発生する
} catch (e) {
    console.error(e.message);  // "creditCardInfo is required when isPremiumUser is true"
}

この例では、ConditionalRequiredデコレーターを使用して、isPremiumUsertrueの場合にのみcreditCardInfoが必須であることをバリデーションしています。このように、プロパティ間の依存関係に基づく複雑なルールを簡潔に実装することができます。

複数条件の組み合わせバリデーション

次に、複数の条件を組み合わせてバリデーションを行う例を紹介します。例えば、ユーザー名が一定の長さであり、特定のパターンに従っている必要があるといったケースです。

function ValidateUsername(target: any, propertyKey: string) {
    let value: string;

    const getter = () => value;
    const setter = (newValue: string) => {
        if (newValue.length < 5 || newValue.length > 15) {
            throw new Error(`${propertyKey} must be between 5 and 15 characters`);
        }
        const usernameRegex = /^[a-zA-Z0-9_]+$/;
        if (!usernameRegex.test(newValue)) {
            throw new Error(`${propertyKey} must contain only alphanumeric characters or underscores`);
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class User {
    @ValidateUsername
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

try {
    const user = new User('abc');  // 長さのバリデーションに引っかかる
} catch (e) {
    console.error(e.message);  // "username must be between 5 and 15 characters"
}

try {
    const user = new User('valid@username');  // 正規表現のバリデーションに引っかかる
} catch (e) {
    console.error(e.message);  // "username must contain only alphanumeric characters or underscores"
}

この例では、ユーザー名が5文字以上15文字以内であり、さらにアルファベット、数字、アンダースコアだけを含む文字列でなければならないという複数の条件をデコレーターでバリデーションしています。

カスタムエラーメッセージのロジック追加

ユーザーに対してより詳しいフィードバックを提供するため、カスタムエラーメッセージを生成するバリデーションロジックをデコレーターに追加できます。エラーの内容を動的に変更したり、複数のエラーメッセージを組み合わせて表示することが可能です。

function ValidatePassword(target: any, propertyKey: string) {
    let value: string;

    const getter = () => value;
    const setter = (newValue: string) => {
        const errors: string[] = [];
        if (newValue.length < 8) {
            errors.push('Password must be at least 8 characters long');
        }
        if (!/[A-Z]/.test(newValue)) {
            errors.push('Password must contain at least one uppercase letter');
        }
        if (!/[0-9]/.test(newValue)) {
            errors.push('Password must contain at least one number');
        }
        if (errors.length > 0) {
            throw new Error(errors.join(', '));
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class Account {
    @ValidatePassword
    password: string;

    constructor(password: string) {
        this.password = password;
    }
}

try {
    const account = new Account('weak');  // 複数のエラーメッセージが生成される
} catch (e) {
    console.error(e.message);  
    // "Password must be at least 8 characters long, Password must contain at least one uppercase letter, Password must contain at least one number"
}

この例では、@ValidatePasswordデコレーターを使用して、複数のバリデーション条件に違反した場合にカスタムエラーメッセージを生成します。これにより、ユーザーに対して詳細かつ複数のバリデーション結果を一度に提示することが可能です。

APIリクエスト用のデータバリデーション

APIリクエストで受け取るデータに対する複雑なバリデーションも、デコレーターを使って実装できます。たとえば、送信されるデータが必須項目を満たしているか、数値の範囲が正しいかをチェックすることが可能です。

function ValidateApiData(target: any, propertyKey: string) {
    let value: any;

    const getter = () => value;
    const setter = (newValue: any) => {
        if (!newValue.name) {
            throw new Error('name is required');
        }
        if (typeof newValue.age !== 'number' || newValue.age < 18 || newValue.age > 60) {
            throw new Error('age must be a number between 18 and 60');
        }
        value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
    });
}

class ApiRequest {
    @ValidateApiData
    data: { name: string; age: number };

    constructor(data: { name: string; age: number }) {
        this.data = data;
    }
}

try {
    const request = new ApiRequest({ name: '', age: 15 });  // バリデーションエラー
} catch (e) {
    console.error(e.message);  // "name is required, age must be a number between 18 and 60"
}

この例では、APIリクエストのデータに対するバリデーションを行い、nameフィールドの必須チェックと、ageの数値範囲チェックをデコレーターで実装しています。

これらの応用例により、複雑な入力チェックやバリデーションが必要な場面でも、デコレーターを用いることで簡潔に処理を実装することが可能です。

次の章では、デコレーターを用いたバリデーションのまとめを行います。

まとめ

本記事では、TypeScriptにおけるデコレーターを活用した型安全なバリデーションの実装方法を解説しました。デコレーターの基本的な使い方から、クラスやプロパティ、メソッドに適用するバリデーション、さらにはカスタムバリデーションや複雑な入力チェックの実装例を紹介しました。

デコレーターを用いることで、コードをシンプルかつ効率的に保ちながら、バリデーションロジックを柔軟に追加できます。型安全を担保しつつ、入力チェックを強化する手法として、デコレーターの活用は非常に有効です。

デコレーターを使ったバリデーションの設計と実装を通して、より堅牢なTypeScriptアプリケーションを構築する助けとなるでしょう。

コメント

コメントする

目次