When writing code, it is often just as important to decide what you want to hide, as well as what you reveal. Most languages provide some method of preventing changes to private variables directly, while others like python have a convention for marking private variables and trusting you not to actually do anything to them.

Consider the repository pattern we discussed recently. It controls how to write and read data from some storage system, and thus will need a mechanism to talk to that system. If that mechanism was publicly accessible it would mean anyone could talk to the storage system directly, ignoring what the repository provides. Not ideal.

There are plenty of reasons to create private variables, but what happens if the language you’re using doesn’t support them? Enter the closure. However, before we can discuss what one actually is, we need to know a thing or two about scope.

So what’s a scope?

Variable names are used to reference their content, but what happens if you have multiple variables that share the same name? If there was no way to differentiate one from another, then if you added new code that happened to use the same name as something used elsewhere, it’d overwrite it.

A scope is the environment in which a variable definition is considered “valid”, in that it will reference the “expected” value. This is commonly the current block of a code that is being executed and any child blocks within it. (GLOBAL scopes are a different matter and as their name implies, are usually available anywhere.)

Take the following TypeScript code for example. What would you expect to be printed to console?

if (true) {
  const foo = "buzz";
  console.log(foo);
}
console.log(foo);

You might already know that this would error when it tried to execute the second console.log, as foo is defined within the if-statement’s block, and thus is “out-of-scope” outside it.

But what about this?

const foo = "fizz";
console.log(foo);
if (true) {
  const foo = "buzz";
  console.log(foo);
}
console.log(foo);

If you’d said “it’ll output: fizz, buzz, fizz”, then gold star for you. ⭐️. Cleverclogs.

But one can easily expect the output to be “fizz, buzz, buzz”. Why is it not? The answer is that the second let foo = ... is defined within the scope of the if-statement, meaning that definition falls out of scope as soon as the if-statement is exited.

The above example is also known as “shadowing”, one variable in a child scope temporarily replaces the one in a parent scope. Try to avoid it if you can as it usually just leads to bugs when you remove one or the other!

..and then a closure?

So what’s this got to do with closures? Well, this is where it gets interesting. What happens to scopes when you also create a function within them?

const buildFooPrinter = () => {
  const foo = "buzz";
  const fooPrinter = () => {
    console.log(foo);
  };
  return fooPrinter;
}

const printer = buildFooPrinter();

printer();
const foo = "fizz";
console.log(foo);
printer();

What would be printed now? If you said “buzz, fizz, buzz”, you get an extra star. ⭐️. Good for you.

But what’s actually happening here? Let’s step through it.

buildFooPrinter is a function, and its body is a block. So it creates a scope and anything defined within it will be limited to that scope. fooPrinter is then defined within that scope, which means it has access to everything in the parent scope, in this case the constant foo. Thus the function it returns is forever scoped to the parent, no matter what you do. Even if you define a new variable that has the same name, it cannot see it. It’s not within the same scope.

A closure is a combination of an environment and a function.

Neat. Now, what can we do with them?

Making variables private with closures

Earlier, I mentioned the repository pattern will have some mechanism of talking to a datastore. How might we hide that with a closure?

const databaseConnection = getDatabaseConnection();

const buildUserRepository = () => {
  return {
    getUser: async (id: string) => {
      // This is safe? Right? Definitely not subject to sql injection...
      return databaseConnection.query(`SELECT * FROM users WHERE id=${id}`);
    }
  }
};

In the above example we’re defining a connection and then the repository, but anything could talk to the database! Instead, what if we did this:

const buildUserRepository = () => {
  const databaseConnection = getDatabaseConnection();
  return {
    getUser: async (id: string) => {
      // This is safe? Right? Definitely not subject to sql injection...
      return databaseConnection.query(`SELECT * FROM users WHERE id=${id}`);
    }
  }
};

Now the only way to talk to the database is by using the repository itself, databaseConnection doesn’t exist anywhere else. You cannot reference it.

And that’s it! A closure is a combination of a function call and an environment, and while their creation can often happen unintentionally, their primary conscious usage is to enable privacy.

Cool.

Updated: