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.
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`
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
}
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
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)
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)
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()
})
Libraries
Here are some real-world libraries that use tag functions:
- Using styled components tag functions, React elements can be styled with CSS:
const StyledButton = styled.button`
font-size: 1em;
margin: 1em;
`
- In Apollo, GraphQL schema definitions can be created using tag functions.
const typeDefs = gql`
type Query {
hello: String
}
`
References
- MDN Template literals - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
- node-postgres - https://node-postgres.com/features/queries
- SQL injection - https://owasp.org/www-community/attacks/SQL_Injection
- Cover image by Andrea Bohl from Pixabay
Top comments (0)