DEV Community

Cover image for JS custom string interpolation
Arooran Thanabalasingam
Arooran Thanabalasingam

Posted on

String Interpolation JavaScript JS custom string interpolation

Table of Contents

How it works

Since ES6, we have been able to use the powerful template literals instead of the clumsy string concatenations.

let age = 254

// before ES6
console.log('The building is ' + age + ' years old.')
// > The building is 254 years old.


// since ES6
console.log(`The building is ${age} years old.`)
// > The building is 254 years old.

console.log(hex`The building is ${age} years old.`)
// > The building is fe years old.

console.log(reverse`The building is ${age} years old.`)
// > The building is 452 years old.
Enter fullscreen mode Exit fullscreen mode

Let me show you how to create your own fancy string interpolation, such as hex and reverse. For that, a special kind of function has to be declared that can receive the strings and the expressions individually. This kind of function is called a tag function.

The first parameter strings: TemplateStringsArray contains all the plain texts, and the second parameter ...expr: any[] has the inserted expressions. (Note that the second parameter should be an array, but it needn't be an array of any.) Furthermore, there are no restrictions on what the tag function should return.

No parentheses are needed to invoke a tag function. That is why it looks like a prefixed template literal rather than a function call. Just this small detail enables us to read the code in a more intuitive way.

function tag(strings: TemplateStringsArray, ...expr: any[]): any {
    /* ... */
}

let description = tag(`I am ${age} years old`)
let description = tag`I am ${age} years old`

Enter fullscreen mode Exit fullscreen mode

Below are the implementations of hex and reverse tag functions:

function hex(
    strings: TemplateStringsArray, 
    ...numbers: number[]
): string {
    let result = strings[0]

    for(let i = 0; i < numbers.length; i++){
        result += numbers[i].toString(16)
        result += strings[i+1]
    }

    return result
}

function reverse(
    strings: TemplateStringsArray, 
    ...expr: any[]
): string {
    let result = strings[0]

    for(let i = 0; i < expr.length; i++){
        const charArray = Array.from(expr[i].toString())
        result += charArray.reverse().join('')
        result += strings[i+1]
    }

    return result
}
Enter fullscreen mode Exit fullscreen mode

Secure, convenient SQL statements

In the following example, the postgresql driver pg will be used to demonstrate the idea. Nevertheless, it can definitely be done with any other SQL drivers with similar capabilities.

npm install pg @types/pg
Enter fullscreen mode Exit fullscreen mode

Typical trap

A common practice is to build SQL statements based on user input, as shown below. However, this is very insecure because user input can be malicious. (For more information about SQL injection, see the OWASP page)

// friendly input
let userInputCountryCode = "DE"

// malicious input
let userInputCountryCode = "DE'; DROP TABLE places"

const statement = "SELECT name FROM places \
                   WHERE country LIKE '" + userInputCountryCode  + "';"

client.query(statement)
Enter fullscreen mode Exit fullscreen mode

Inconvenient way

Most database systems have a feature called a prepared statement or parameterized query. This feature can also be used to protect against SQL injections.

Parameterized queries are much more secure because the statement and the inserted expressions are transferred separately to the database server. Thereafter, the unaltered expressions are sanitized with battle-tested mechanisms within the server. In the end, the sanitized expressions are substituted into the statement.

The following example shows how parameterized queries can be invoked with the pg driver:

const statement = "SELECT name FROM places \
                   WHERE country LIKE $1   \
                   AND name LIKE $2;"
let values = ["DE", "P%"]

client.query(statement, values)
Enter fullscreen mode Exit fullscreen mode

Convenient and secure way

Imagine you have to substitute numerous expressions into an SQL statement. Exactly as the number of expressions increases, it becomes more difficult to maintain the SQL statement.

One way to regain convenience is to create a custom string interpolation. As demonstrated below, the safeQuery tag function puts indexed dollars such as $1 where the expressions should be placed. After that, the prepared statement and the unaltered expressions are passed to the parameterized query function of pg.

const safeQuery = 
  (client: Client) => 
  (strings: TemplateStringsArray, ...expr: any[]) => {
    let statement = strings[0]

    for(let i = 0; i < expr.length; i++){
        statement += '$' + (i+1)
        statement += strings[i+1]
    }

    return client.query(statement, expr)
}

client.connect()
let countryCode = 'DE'
let firstLetter = 'P%'

const resultPromise = 
    safeQuery(client)`SELECT name FROM places 
                      WHERE country LIKE ${countryCode} 
                      AND name LIKE ${firstLetter};`

resultPromise.then(result => {
    console.log(result.rows)

    client.end()
})

Enter fullscreen mode Exit fullscreen mode

Libraries

Here are some real-world libraries that use tag functions:

const StyledButton = styled.button`
  font-size: 1em;
  margin: 1em;
`
Enter fullscreen mode Exit fullscreen mode
  • In Apollo, GraphQL schema definitions can be created using tag functions.
const typeDefs = gql`
  type Query {
    hello: String
  }
`
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)