-1

I've been exhausting my Google-fu trying to find examples on how to best assert parameters in ES6 module methods, or deal with the uncertainty of the type of data that may get passed around. Namely between things like strings and numeric types. I've just been scratching my head for a few hours with a weird behaviour issue around a computed property and MobX where I have some test code that initializes a default sent of values and that works just fine. The computed value takes an initial value, sums up two totals from related data (credits and debits) then adds the debits and deducts the credits to return the adjusted value. So the return initialValue (1000) + totalDebits (0) - totalCredits (0) which returns 1000. Simple. However when I added a dialog to enter a new item to the list with an initial value of 2000, my computed value was coming back as 20000! What was stranger is that I persist these objects to local storage and when I refresh from the stored values the computed total still had 20000, but the other existing values had the correct computed values matching their initial values.

I finally did track down the reason for it, and verified it in the persisted state that my initialBalance from the entered page was being stored as "2000", a string literal. "2000" + 0 - 0 which added a 0 to the string, while the minus was ignored.

My question is what options are there with plain JS or suitable ES6 library for handling types that might be coming into a function such as a MobX action? Plus any advice for best practices when dealing with JS arguments in general.

For example: In my component I have an onChange event

onInitialBalanceChange(e){
    const value = e.target.value;
    const account = this.state.account;
    let validationError = null;
    let isBalanceValid = true;
    if(!this.validateInitialBalance(value)){
        validationError = 'The initial balance must be a positive number.'
        isBalanceValid = false;
    } 
    account.updateInitialBalance(value);
    this.setState({validationError: validationError, isBalanceValid: isBalanceValid, isComplete: isBalanceValid && this.state.isNameValid});
}

the updateInitialBalance is registered as a MobX action:

updateInitialBalance(initialBalance){
    if (!!initialBalance) this.initialBalance = initialBalance;
    return this;
}

This is where my question/concern is, and my surprise that I haven't really seen much out there on addressing JS argument types and type conversion beyond explanations of functions that do individual checks. My component does a validate check on the balance, and could return a balance as number, but that still leaves the update method potentially still being called elsewhere with a numeric string. I have an assertion that the argument isn't null/empty, but what are the best practices for asserting/converting it's type other than a significant # of if() conditions? initialBalance may be a string "2000" or a number 2000 and it should also guard against invalid data such as "fred"?? So I put the question out there for reliable type checking in a duck-typed world.

From what I can see this appears to work, but it just feels a bit "wrong" based on the documentation:

if (!!initialBalance) this.initialBalance = parseInt(initialBalance);

parseInt takes in "string", however if initialBalance is already a number (not a numeric string) this appears to work as well.

I know this is a bit of an opinionated question, but I'm really looking for options because the screaming in my ears from the C# developer in me is getting deafening. :)

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • 1
    What's wrong with [`Number(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) and checking for `NaN`? – Herohtar Jun 07 '19 at 04:38
  • The issue isn't so much checking for numeric or parseInt vs Number (Though in hindsight Number() is probably a better fit.) The concern I have with using just these methods is that they expect strings in. My method might get a string, but it might get a number. Right now if I pass parseInt (and probably Number())a value that is already a number (not a string) it works, but is that considered reliable? The last thing I want to do is something like ` = Number(initialBalance.toString())` because one day ES may "really" expect a string. – Steve Py Jun 07 '19 at 05:41
  • Those functions don't "expect" anything -- they accept any type of object. `parseInt` may be intended for parsing a string, but it will happily take an int and return a valid value. `Number` just tries to convert the given object to a number; if it fails, your object wasn't a number in any form. You don't need to do any more checking than that. – Herohtar Jun 07 '19 at 06:18

1 Answers1

1

It sounds like there are three questions here. These are just some opinions that I think ought to help with each.

How should I best access local storage?

For storing and retrieving values from Local Storage you probably want to write a wrapper module of some kind, that does something like this:

class LocalStorageWrapper {

    setInitialBalanace(balance){
        if(Object.prototype.toString.call(balance) !== '[object Number]') return false;
        // any other validation. > 0? < 9999999? etc
        // set the value
    }

    getInitialBalance(){
        const val = // get the value from local storage
        if(!isNaN(parseFloat(val)) && isFinite(val)) return parseInt(val);
        if(Object.prototype.toString.call(val) === '[object Number]') return val;
        // return some default value. The value is either unset or corrupted
    }
}

Notes:

  • Use this to access localstorage throughout your codebase to save you from repeating these checks everywhere. Also unit test the crap out of it.
  • Object.prototype.toString.call(balance) !== '[object Number]' is IMO the best way to test for a meaningful number. See here for test cases.
  • !isNaN(parseFloat(val)) && isFinite(val) is the best way to test for a 'numeric' value that can reliably be parsed to a number.

Is there a handy pattern to check for user errors

For generic validation I think it's usually safer to throw a very specific error and catch them it the end. For example:

onInitialBalanceChange(e){

    try {
        if(!this.validateInitialBalance(value)){
            throw new Error('initial_balance_negative')
        }
        // other error checks here...
        account.updateInitialBalance(value); // do whatever you want
        this.setState({validationError: null, isBalanceValid: isBalanceValid, isComplete: isBalanceValid && this.state.isNameValid});
    } catch(e) {
        // ideally match against some expected
        if(constantStrings.hasOwnProperty(e.message)){
            this.setState({validationError: constantStrings[e.message]}); // an 'expected' user error
        }else{
            this.setState({validationError: 'Unknown error'}); // a 'true' error
        }
    }

}

// somewhere else in 'strings.js' or something
const constantStrings = {
    initial_balance_negative: 'The initial balance must be a positive number.'
};

Duck-typed JS is hard

I know :(. But I strongly recommend Typescript. The learning curve really isn't too steep and if it's an option for you project, use it. It'll catch plenty of errors for you ahead of time (including the local-storage-is-a-string-value-store bug) and save all the trouble unit testing these things.

RP-3
  • 684
  • 4
  • 22
  • Yeah, I was going to comment on Typescript. I've used it in the past and certainly preferred it, but in this case I'm sticking to ES6~8 since that is what the majority of the market is still using. A lot of this is just trying to build up a reserve of knowledge for identifying these kinds of issues, recognizing good code and the various trade-offs with different approaches. For exceptions, I'm a bit wary of throwing for known items like validations. Coming from .Net Throwing exceptions incurs a cost in the form of generating the call stack which I expect happens in JS as well. – Steve Py Jun 07 '19 at 05:34
  • That's fair enough. The non-try-catch idiomatic way to do this in JS is then probably to use a callback. I.e., const handleInitialBalanceChange(cb) => { return cb(errorMessage, null); // if error return cb(null, data) // if no error } and then use it like: handleInitialBalanceChange((err, data) => { if(err) return setState({ err: err }) return setState({ data: data }) }) It's totally up to you. I usually find that with front-end work the performance cost of throwing is negligible vs the benefit of single-codepath readability. – RP-3 Jun 07 '19 at 05:47
  • Note: after around Node 8.3 (V8 6), `try`/`catch` has pretty minor performance impact. It's in common idiom now that `async`/`await` is more frequently encountered. Normally I return or set an error state for validation, but throw for something that should be considered programmer error. – Rich Churcher Jun 11 '19 at 06:18