Types in JavaScript

JavaScript's type system is based on the simple idea of copying from an example.

You're probably aware that types in JavaScript are prototype-based. You may not be aware of exactly how cool that is. Prototypes (also known as exemplars and pioneered in langauges like Self), create new values by copying an existing value. There's no need for a separate abstraction, like classes, to represent types. You simply designate a value as a prototype (exemplar) and make copies of it.

Unfortunately, JavaScript tends to obscure this simplicity. The (well-intentioned) introduction of classes in ES6 is one example. The lack of a standard interface for identifying a value's prototype is another. ES6 (quietly) addressed this latter problem with introduction of Object.getPrototypeOf. Which means we can reliably write code that knows about prototypes.

A Deeper Understanding Of Prototypes

Let's take advantage of this to explore how types really work in JavaScript. We don't really need classes in JavaScript. They're there if we want to use them. But in a functional world, we have the freedom to discard them in favor of a pure prototype-based model. (For which classes are just sugar-coating anyway, at least the way they're implemented in ES6.)

The Basics

First, let's define a simple wrapper around Object.getPrototype. (As always, our code is in CoffeeScript but these functions all work fine in JavaScript, and semantically, there's no difference.)

prototype = (value) -> if value? then Object.getPrototypeOf value

Next, let's define a function that allows us to check the prototype.

isPrototype = curry (p, value) -> p? && p == prototype value

We can now check the type of a value by comparing it to the prototype property of a given type. This works because, by convention, a “type” in JavaScript is, in fact, an object with a prototype property. That's the property we copy when we want to create a new instance of that type.

isArray = isPrototype Array.prototype

assert isArray [1,2,3]
assert !isArray 7

We take advantage of the fact that we can curry isPrototype to define isArray a simply a curried version of isPrototype.

So far, so good. But we can still clean this up a bit.

isType = curry (type, value) -> isPrototype type?.prototype, value

Now we can easily define type-checking helpers. We take advantage of currying again, only now we can just pass in the type, instead of the prototype.

isArray = isType Array

assert isArray [1,2,3]
assert !isArray 7

Prototype Chains

You're probably also aware of the so-called prototype chain and that JavaScript uses it instead of inheritance. All this means is that we define one prototype by copying another. If we have an prototype for a bank account, we can copy that to create a prototype checking account. After all, in a sense, we're creating a bank account. It's just a bank account that we're going to tweak so that we can use it as a prototype for a checking account.

However, if we want to answer the question whether a given value is a bank account, we can no longer simply rely on the value's prototype. Because its prototype might be our prototype checking account. Which is not the same as our prototype bank account. What we need to do is check the entire chain of prototypes to see if we eventually reach the bank account prototype.

Property Lookups

JavaScript follows this same algorithm to resolve property references. It first checks the value directly for the given property. Then it checks the value's prototype. Then it checks the prototype of that prototype. And so on, until it reaches a value with no prototype (that is, the value of the prototype is null.)

BankAccount = prototype: number: 0
CheckingAccount = prototype: Object.create BankAccount.prototype
myAccount = Object.create CheckingAccount.prototype
assert myAccount.number == 0

Even though we never defined a number property on myAccount or on the CheckingAccount prototype from which we created it, it's still there. Of course, that's because we defined it on the prototype for BankAccount, which we used to define the prototype for CheckingAccount.

Type Lookups

Of course, we want our helper functions to be able to take this prototype chain into account. What about instanceof? The problem with instanceof is that, for historical reasons, it doesn't work as reliably as we'd like.

# huh?
assert !(7 instanceof Number)

So we need to define a function to allow us to check the prototype chain directly. This is analogous to our function that checks the prototype.

isTransitivePrototype = curry (p, value) ->
  p? && (p == (q = prototype value) || (q && isTransitivePrototype p, q))

We check to see if the given prototype is in fact that prototype for the given value. If not, we call isTransitivePrototype recursively with the value's prototype, effectively following the prototype chain.

Again, taking advantage of the JavaScript convention that a type is an object with a prototype property, we can define a type-centric variant of the same function. This is the prototype-chain tracing analog to isType.

isKind = curry (type, value) -> isTransitivePrototype type?.prototype, value

We can now define type-checking helpers based on the prototype chain.

isBankAccount = isKind BankAccount
assert isBankAccount myAccount
assert !isBankAccount, number: 1

By currying isType and isKind we can easily define type-checking functions. But what about defining new types? Can we make that a little easier?

Defining Types

One solution, of course, is to use JavaScript classes. But recall that classes are really just syntactic sugar around prototypes. Instead of obscuring the elegance of JavaScript's protoypes, why don't we take advantage of it?

Let's define two helper functions for defining new types and creating instances of those types.

Type =
  create: (type) -> if type? then Object.create type.prototype
  define: (parent = Object) -> prototype: Type.create parent

Now we can define our bank account and checking account types more succinctly.

BankAccount = Type.define number: 0
CheckingAccount = Type.define BankAccount
myCheckingAccount = Type.create CheckingAccount
assert isType CheckingAccount, myCheckingAccount
assert isKind BankAccount, myCheckingAccount

Initialization

You've probably noticed by now that we're not using constructor functions or the new operator anywhere. One reason we're avoiding this is that we can (accidentally) call constructor functions like ordinary functions. (That is, without using the new operator.) This can lead to unexpected bugs which are difficult to find. We can prevent these by checking the value of this in the constructor function, but that's still error-prone and awkward. And there are other reasons to avoid using new anyway.

Instead, we construct objects using our Type.create function . But what if we want to initialize the value? For example, what if we want to assign a unique account number to each account upon creation. Simple: just add a create method to your type object.

CheckingAccount.generateAccountNumber = do (n = 0) -> -> n++
CheckingAccount.create = ->
  account = Type.create CheckingAccount
  account.number = CheckingAccount.generateAccountNumber()
  account
firstAccount = CheckingAccount.create()
secondAccount = CheckingAccount.create()
assert firstAccount.number != secondAccount.number

Shared Initialization

This works fine for a single type. But what if we want to ensure unique account numbers across all BankAccount values? If we were using classes, we'd reference the super class constructor, which would take care of setting the account number. But with prototype chains, there's no “super” class. So how can we allow CheckingAccount to take advantage of common initialization details implemented by all bank accounts?

The most general solution is to explicitly call the desired initialization function. That is, just call something along the lines of BankAccount.initialization. While this isn't as succinct or expressive as super, there's nothing wrong with being explicit.

BankAccount = Type.define()
BankAccount.generateAccountNumber = do (n = 0) -> -> n++
BankAccount.initialize = (account) ->
  account.number = BankAccount.generateAccountNumber()
  account
CheckingAccount = Type.define BankAccount
CheckingAccount.create = ->
  BankAccount.initialize Type.create CheckingAccount

Arguably, this is actually easier to reason about than super.

Making Copies

We can now easily define types based on prototypes, check the prototype of a value, and check the prototype chain. We can even check the type or ancestor type of a value. All without polluting JavaScript's simple and elegant prototype-based type system.

And, of course, all these function are available in Fairmont, our functional reactive programming library. But whether you use Fairmont or not, these basic ideas are both fundamental to JavaScript and can help you understand the real underpinnings of JavaScript's approach to types.

Notes