Ramblings of an old-school software developer, father, and woodworker.

Recent Posts

Ninjas and Landmines
A retelling of a classic anecdote

Are Static Types Useless
Static types means more than number vs. string.

Contract Negotiations
As a software freelancer, how should contract negotiations look?

Freelance Contract Legal Clauses
Some clauses that I use in my contract as a freelancer.

Are Static Types Useless

· by Tim Mensch · Read in about 10 min · (2065 Words)
blog developer languages post programming security server software engineering typescript

So I write a lot on Quora about why I like static types. I’ve also written about them on this blog. But I see a lot of responses that indicate to me a profound misunderstanding about what “static types” really means.

In the context of JavaScript vs. TypeScript in particular, I think that people who haven’t yet extensively used TypeScript think that the difference is about the compiler telling the difference between strings and numbers. That is a feature of type safety, but it’s actually not even the most important feature.

Static types grow in importance as your data structures grow more complex. Yes, keeping your data types as simple as possible is a great goal, but any project that becomes at all sophisticated will benefit greatly from having static types. Especially inferred static types.

Types also include “options” structures being passed in to libraries you’re using. Getting those options named correctly, and passing in a string where they expect a string and an array of strings or a sub-object where appropriate? Those are all important if you want your code to actually work.

And when there are types defined for the code you’re developing, you never need to remember exactly how the data you’re working with is structured; it’s all right there in the editor. Code completion only offers exactly what is available instead of a scattershot of “common member functions”. There’s no running the code to figure out what you really get, because it’s all documented. There are no stupid mistakes that you have to go back, find, and fix when your tests fail. My development velocity is seriously 2-4x faster with static types, and I was no slouch before.

Maybe Some Examples Would Help?

Keep in mind these are simplified examples. Actual data comes in far more flavors and shapes, and often has even stranger relationships. So if you look at these examples and think, “This is obvious!”, then you’re not using your imagination sufficiently – some data structures get really hairy due to inherent complexity of the problem domain.

If you’ve simply never worked with data that complicated, and you are sure you never will, then maybe this advice doesn’t apply to you. But ignore it at your own peril, because someday you may be handed a genuinely complex problem, and when your dynamic type tools fail you’ll only have yourself to blame.

Say you have this set of data structures (I’ve added lots of comments in case you’re not familiar with TypeScript):


// A string type that can only be one of three values.
type EntryType= "Personal" | "Work" | "Other";

// An interface is the "shape" of an object. So in this case,
// only objects that have a member `lines` that is a string array
// and a member `type` that is one of the three above strings
// would be valid.
//
// Defining an interface doesn't actually change the generated
// JavaScript code at all; it's only used at compile time to verify
// the types and provide compiler-verified documentation.
//
// This interface is a simplified mailing address with an arbitrary
// number of address lines.
interface Address {
    // Address lines.
    lines: string[];

    // What kind of entry is this, Personal, Work, or Other?
    type: EntryType;
}

interface Email {
    // The actual email address
    address: string;

    // What kind of entry is this
    type: EntryType;
}

interface Phone {
    // Actual phone number
    number: string;

    // What kind of entry is this
    type: EntryType;
}

// Putting the above together into a Person object. Every
// Person can have addresses, emails, and/or phone numbers,
// but we don't require a person to have any of the above.
interface Person {
    name: string;

    // Optional addresses member; can be Address[] or undefined
    addresses?: Address[];
    // Optional emails member
    emails?: Email[];
    // Optional phones member
    phones?: Phone[];
}

A Person can have some number of physical addresses, some number of email addresses, and some number of phone numbers. Or none of any of them.

Note that since documentation of your APIs is widely considered a mandatory best-practice, TypeScript is simply creating a way to employ DRY to those interfaces in a standardized manner that the compiler itself can validate. Which is why I don’t accept any arguments that adding types to JavaScript “requires more work”; the only “more work” it requires is keeping your “documentation” up to date, and not actually violating any of its rules.

Now say you’re writing some code:

function logPersonPlusAddresses(person: Person) {
    // Print the person's name. Works fine, and is safe
    // because this function can only be called with a Person-compatible
    // object, and not null, or undefined, or anything else.
    console.log(person.name);

    // Iterate through the addresses and print each one.
    // In TypeScript, this shows up as an error right in your editor!
    person.addresses.forEach(a=>console.log(a)); //ERROR
}

Maybe the Person you’re testing with has addresses, and your test code works fine. But since addresses can be undefined according to the interface, TypeScript sees that you’re dealing with a “maybe undefined” value and prevents you from doing anything with it that isn’t compatible with the undefined type.

Yet, changing the code to the safer version below just works with no type additional annotations or casting required:

function logPersonPlusAddresses(person: Person) {
    // Print the person's name. Works fine, and is safe
    // because this function can only be called with a Person-compatible
    // object, and not null, or undefined, or anything else.
    console.log(person.name);

    if (person.addresses) {
        // TypeScript uses code tracing to determine that person.addresses!=null
        // in this block, so it knows the following call is safe.
        person.addresses.forEach(a=>console.log(a));
    }
}

TypeScript sees the if statement and realizes that, in the following block, person.addresses is not undefined, and therefore is the only remaining option – an array of Address objects.

Functional Benefits

How about functional code? Say you’re using Lodash and you want to find all of the phone number area codes used by everyone in your address book. (Assuming US phone numbers in the 123-456-7890 format here; this is just an example, so please bear with me.) So I write out the following code, and oops, there’s a mistake:

Showing a non-obvious error

For people less familiar with the “functional” approach, I’ve tried to document all of the parts above, but briefly:

  • Start a chain of commands using the phoneBook this function has been passed (which we also are guaranteed is an array of Person objects).
  • Convert it from an array of Person objects to an array of arrays of Phone objects (some of which may be undefined, because the phones member is optional).
  • Remove the undefined entries from that array. Now it’s all an array of Phone[] arrays.
  • Flatten the arrays: Flatten takes an array of arrays and makes one big array with all of the elements. Now it’s one big flat array of Phone objects.
  • Try to “slice” the first three characters off of each phone number – this is where the error is, because while writing this I “forgot” that it was Phone objects and not just strings, and our Phone object doesn’t have a slice method.
  • Sort and make the result only have unique area codes.

Now as I show below, this error wouldn’t be likely when using TypeScript, because your editor should tell you what methods are available on phone, but say you’re typing it out quickly and didn’t notice what the editor was suggesting. As I’ve laid it out here, it’s pretty obvious that we’re dealing with Phone objects, but at the same time that’s the kind of thing someone is likely to forget when they’re coding – and storing a phone number as a string may just be assumed, especially if you’re using someone else’s library.

Once you see that red-squiggle and you’re wondering what’s up, all you have to do is hover over the phone to understand:

TypeScript Knows Your Types

In fact, when you delete .slice and type phone. it immediately shows you what types are available there:

TypeScript Really Knows Your Types

Correcting the .slice to .number.slice, our final working code looks like:

function getAllAreaCodes(phoneBook: Person[]) {
    return _.chain(phoneBook)
        .map(person => person.phones) // Convert to only "Phone[] or undefined" entries
        .compact() // only Phone[] entries (skipping undefined entries)
        .flatten() // one big Phone[]
        .map(phone => phone.number.slice(0, 3)) // just the area code
        .sort() // sort it
        .sortedUniq() // only keep unique values in the result
        .value(); // This converts a chain back into a value -- in this case a string[]
}

This gives you a taste of the power of inferred types (and generics) in TypeScript. When writing code like this it’s easy to forget exactly what object or value you have at each stage. TypeScript can keep track of the transforms, though, so it adds a profound level of intelligence to your editor.

As an aside, this kind of type precision is simply not possible in a user-made library in Go, which is one reason I’ve stopped recommending Go – its type system is a holdover from the ‘90s, and I’d really rather use a 21st century type system. (There is also apparently a large list of reasons not to use Go, but I gave up on the language before running into most of them myself.)

Since TypeScript clearly tracks the type of the values through that entire transformation chain, it also knows the return result is string[]. Implicit types are really powerful; most of the time you can get away with only specifying the type coming in to a function or method, and the rest are just inferred. There are a few exceptions, but as TypeScript gets more mature, those exceptions are getting quite rare.

There’s enough precision in the above kind of programming that, for simple tasks like the one described above, once all the types line up correctly, everything tends to just work. I wrote out the above code for this example in VS Code without testing it at all, but I’m pretty confident that it will work without needing to test it extensively.

In reality I’d probably test it once to make sure I didn’t misremember how slice worked, but once it’s been verified? We’re done. With code written like this, and with type safety keeping it wired correctly, tests would be largely redundant. In fact, I’d say any tests I wrote to verify it works would really be testing Lodash and not my own logic.

So far I’m talking about avoiding errors while writing fresh code, but the same power helps you refactor code. Renaming a method on an API? Hit one key, type the new name, hit enter. Done. More complex refactoring, where functionality that was deeply embedded in a class needs to be pulled out? Several such actions are already built in to VS Code, including pulling a function into a new file, where it will also add any imports required to get it to compile.

And really complicated refactoring? The way I’ll usually do it is to move the code to where I want it and keep fixing things until all the code compiles. Seriously, that’s it. I’ll run through it once in a debugger to ensure I didn’t miss anything, but 98% of the time, “it compiles” is the same as “it works.” I’ve heard many other developers reporting that a similar approach works for them.

Conclusion

Static types save you time, net, and help you keep your code structure explicit, which can help your code stay clean over the long term. This is why I keep talking about static types. Not because I want to keep my strings and numbers separate, though that can help prevent the occasional stupid bug–no, that’s only a small part of why static types are important. If you always create exactly the correct data and code structure the first time, so you never need to refactor, and you have all of the data structures memorized exactly so you never are confused about how to access data and what layer you’re on, and new developers never join your project, so there’s no need for living documentation, and your data always remains simple and shallow, and your tests are so great that they catch all of the kinds of bugs that a type-checker would otherwise prevent? Then sure, you can ignore static types with impunity.

For the rest of us static types should be considered a required best practice.

Comments