1

I'd like to extend javascript to add custom type checking.

e.g.

function test(welcome:string, num:integer:non-zero) {
   console.log(welcome + num)
}

which would compile into:

function test(welcome, num) {
    if(Object.prototype.toString.call(welcome) !== "[object String]") {
        throw new Error('welcome must be a string')
    }

    if (!Number.isInteger(num)) {
        throw new Error('num must be an integer')
    }

    console.log(welcome + num)
}

What's the most straightforward way of doing this?

So far i've looked at:

  • sweet.js (online documentation looks out of date as I think it's going through some sort of internal rewrite)
  • esprima and escodegen (not sure where to start)
  • manually parsing using regular expressons
Tony J Watson
  • 629
  • 2
  • 9
  • 20
  • 1
    Have you considered typescript? – Andy Lamb Apr 26 '17 at 05:32
  • Your last "proposal" wouldn't take you far. You're out to do some serious "hacking" (I think that's what the js culture calls that), so scary APIs and outdated docs shouldn't scare you :) – Bartek Banachewicz Apr 26 '17 at 05:36
  • @AndyLamb Yes I have, what I've got in mind is a lot more powerful and expressive though. For example function (allWages:array[integer:not-negative]:no-empty)... and then also having custom checkers per application. – Tony J Watson Apr 26 '17 at 05:38
  • @BartekBanachewicz The regex solution is complicated.. especially with trying to deal with multiple open brackets, and identifing where to insert the replacement code.... that's why i was interested in trying to get something to work with sweet.js/esprima/escodegen (sweet.js uses escodegen under the hood apparantly) – Tony J Watson Apr 26 '17 at 05:40
  • In dynamically typed languages enforcing types is often handled by test frameworks. – david25272 May 02 '17 at 06:46

1 Answers1

1

After evaluating all the various options, using sweet.js appears to be the best solution. It's still fairly difficult to get working (and I am probably doing stuff the wrong way) but just in case someone want's to do something similar this here was my solution.

    'use strict'

    syntax function = function(ctx) {
        let funcName   = ctx.next().value;
        let funcParams = ctx.next().value;
        let funcBody   = ctx.next().value;

        //produce the normal params array
        var normalParams = produceNormalParams(funcParams)

        //produce the checks
        var paramChecks = produceParamChecks(funcParams)

        //produce the original funcBody code

        //put them together as the final result

        var params = ctx.contextify(funcParams)

        var paramsArray = []
        for (let stx of params) {
            paramsArray.push(stx)
        }

        var inner = #``
        var innerStuff = ctx.contextify(funcBody)
        for (let item of innerStuff) {
            inner = inner.concat(#`${item}`)
        }

        var result = #`function ${funcName} ${normalParams} {
            ${paramChecks}
            ${inner}
        }`

        return result

        function extractParamsAndParamChecks(paramsToken) {
            var paramsContext = ctx.contextify(paramsToken)

            //extracts the actual parameters
            var paramsArray = []
            var i = 0;
            var firstItembyComma = true
            for (let paramItem of paramsContext) {
                if (firstItembyComma) {
                    paramsArray.push({
                        param: paramItem,
                        checks: []
                    })
                    firstItembyComma = false
                }

                if (paramItem.value.token.value === ',') {
                    firstItembyComma = true
                    i++
                } else {
                    paramsArray[i].checks.push(paramItem.value.token.value)
                }
            }

            for (var i = 0; i < paramsArray.length; i++) {
                var checks = paramsArray[i].checks.join('').split(':')
                checks.splice(0, 1)
                paramsArray[i].checks = checks
            }

            return paramsArray
        }

        function produceNormalParams(paramsToken) {
            var paramsArray = extractParamsAndParamChecks(paramsToken)

            //Produces the final params #string
            var inner = #``
            var first = true
            for (let item of paramsArray) {
                if (first === true) {
                    inner = inner.concat(#`${item.param}`)
                } else {
                    inner = inner.concat(#`,${item.param}`)
                }
            }
            return #`(${inner})`
        }

        function produceParamChecks(paramsToken) {
            var paramsArray = extractParamsAndParamChecks(paramsToken)

            var result = #``
            for (let paramObject of paramsArray) {
                var tests = produceChecks(paramObject)
                result = result.concat(#`${tests}`)
            }
            return result
        }

        function produceChecks(paramObject) {
            var paramToken = paramObject.param
            var itemType   = paramObject.checks[0]
            var checks     = paramObject.checks

            if (itemType === undefined) return #``

            if (itemType === 'array') {
                return #`if (Object.prototype.toString.call(${paramToken}) !== "[object Array]") throw new Error('Must be array:' + ${paramToken})`
             else {
                throw new Error('item type not recognised: ' + itemType)
            }
        }
    }
Tony J Watson
  • 629
  • 2
  • 9
  • 20