2
votes

I am working on a TypeScript definition file for the knockout validation plugin. One of the things I'm stuck on has to do with how new validation rules are defined in that library.

The way to create a basic synchronous validator for ko.validation is like so:

ko.validation.rules['myrulename'] = {
    message: 'default message to display when validation fails',
    validator: function (value, params) {
        // use value and params to evaluate rule, 
        // then return true if valid, false if invalid
    }
}

This would be straightforward, however there is a slightly different way to create an asynchronous validator:

ko.validation.rules['myrulename'] = {
    async: true,
    message: 'default message to display when validation fails',
    validator: function (value, params, callback) {
        // use value and params to evaluate rule, 
        // then invoke callback to indicate pass / fail
        // this function does not return a bool (it is void)
    }
}

The asynchronous callback can take either a bool (for validation pass/fail) or an object literal (isValid for pass/fail, and message to change the validation message). For this, I have created the following:

interface KnockoutValidationAsyncCallbackArgs {
    isValid: bool;
    message: string;
}

interface KnockoutValidationAsyncCallback {
    (result: bool): void;
    (result: KnockoutValidationAsyncCallbackArgs): void;
}

Here is a trimmed down version of my KnockoutValidationStatic which exposes the rules array. I have also create a special interface for the rules array to specify string indexing:

interface KnockoutValidationRulesArray extends Array {
    [index: string]: KnockoutValidationRule;
}

interface KnockoutValidationStatic {
    rules: KnockoutValidationRulesArray;
    ... other members defined on this interface
}

interface KnockoutStatic {
    validation: KnockoutValidationStatic;
}

What I am stuck on is the KnockoutValidationRule. I have tried the following:

interface KnockoutValidationRule {
    async?: bool;
    message: string;
    validator(value: any, params: any): bool;
    validator(value: any, params: any, callback: KnockoutValidationAsyncCallback): void;
}

This works fine for synchronous validator declarations. However when I create an asynchronous one, I get the following error(s):

Cannot convert '{ message: string; validator: (value: any,params: any,callback: KnockoutValidationAsyncCallback) => void }' to 'KnockoutValidationRule': Types of property 'validator' of types '{ message: string; validator: (value: any,params: any,callback: KnockoutValidationAsyncCallback) => void; }' and 'KnockoutValidationRule' are incompatible:

Call signatures of type '(value: any,params: any,callback: KnockoutValidationAsyncCallback) => void' and '{ (value: any,params: any): bool; (value: any, params: any,callback: KnockoutValidationAsyncCallback): void; } are incompatible:

Call signature expects 2 or fewer parameters

I have also considered making the callback an optional parameter, but that doesn't work because the function returns different values depending on the signature (bool without 3rd arg, void if 3rd arg is present).

What am I doing wrong here? Is there something wrong with my string-indexed array for the rules? Is it possible to create a TypeScript interface based on how this library works?

Update 1

It looks like I was on the right track with the overloads in my KnockoutValidationRule interface. I just discovered that if I change ko.validation.rules from a KnockoutValidationRulesArray to a KnockoutValidationRule[], my test code compiles fine for both sync and async validators...

interface KnockoutValidationStatic {
    //rules: KnockoutValidationRulesArray; // this breaks it
    rules: KnockoutValidationRule[] // this fixes it
    ... other members defined on this interface
}

Am I declaring the array wrong?

Update 2

Here is some example code I am using to test the definitions:

/// <reference path="../../ko/knockout-2.2.d.ts" />
/// <reference path="../../ko/knockout.validation.d.ts" />

module TestKoVal {

    ko.validation.rules['test1'] = {
        message: '',
        validator: (value: any, params: any): bool => {
            return false;
        }
    }

    ko.validation.rules['test2'] = {
        async: true,
        message: '',
        validator: function (value: any, params: any, callback: KnockoutValidationAsyncCallback): void => {
            callback(false);
            callback(true);
            callback({ isValid: false, message: 'custom' });
        }
    }
}

Like I said, the above compiles fine when ko.validation.rules is a KnockoutValidationRule[]. It fails when I change it to a KnockoutValidationRulesArray.

2
I'm not an expert on this, but try splitting the KnockoutValidationAsyncCallback in two (one for each signature) and create an extra overload on the KnockoutValidationRule. Because you have two methods on the interface, I think TypeScript expects an object with two methods rather than a single matching function. - Morten Mertner
@MortenMertner thanks for the suggestion. The problem with splitting the KnockoutValidationAsyncCallback is that it would prevent code from being able to execute both overloads of the callback. Within a validation function, I might want to invoke either callback(true) or callback({isValid: false, message: 'custom message'}). - danludwig
Could you perhaps post the classes going along with this? I could fiddle a bit with it and see if I can come up with something. - Morten Mertner
PS: Remember to send your .d.ts file for this to the guy maintaining this repo: github.com/borisyankov/DefinitelyTyped - Morten Mertner
@MortenMertner, yes, I was actually going to send another pull request to boris after I have this definition file a little more fleshed out. github.com/borisyankov/DefinitelyTyped/pull/134 - danludwig

2 Answers

0
votes

I think I may have figured out why the above won't work. In javascript, when you use the [] array syntax with a string as the indexer, you are not creating an array in the usual sense. Instead, that syntax simply adds properties to the object. The following are functionally equivalent:

ko.validation['test1'] = { ... };
ko.validation.test1 = { ... };
0
votes

You can work around the problem by modifying the KnockoutValidationRule:

export interface KnockoutValidationRule {
    async?: bool;
    message: string;
    validator: (value: any, params: any, callback?: KnockoutValidationAsyncCallback): bool;
}

And make the corresponding change where this is used:

ko.validation.rules['test2'] = {
    async: true,
    message: '',
    validator: (value: any, params: any, callback?: KnockoutValidationAsyncCallback): bool => {
        callback(false);
        callback(true);
        callback({ isValid: false, message: 'custom' });
    }
}

This unifies the two method signatures. The validation method doesn't actually need to return a bool, so the fact that the method is declared to return a bool is mainly a cosmetic/aesthetic problem.

It may be worthwhile to open a discussion around this on the codeplex site, as I doubt this represents uncommon use in JavaScript libraries.