Search code examples
javascriptsweet.jsesprima

Extend Javascript Syntax to Add Typing


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

Solution

  • 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)
                }
            }
        }