Less noise, more data. Get the biggest data report on software developer careers in South Africa.

Dev Report mobile

How I use TypeScript to Boost My Productivity

13 April 2022, by Christian Gill

There’s a misconception that TypeScript is less productive than JavaScript because it’s safer and more strict. In my experience as a JavaScript developer working with TypeScript, it allows me to be safe and productive. Here’s how I use TypeScript to enhance my productivity.

OfferZen Llama at computer screen with compiler showing an error

I learnt about the productivity of type-driven development while learning Haskell and other strongly typed functional languages like Elm and ReScript. These communities helped me realise the value of types and how much they can help to write correct code. With types, I don’t have to return to the code as much to fix things retroactively.

The illusion of being unproductive

JavaScript, on the other hand, requires you to fix things retroactively. Being a dynamic language, it’s very loose. It gives you a lot of freedom and thus favours a “happy path” approach to development: You work quickly with minimal effort and see results immediately. This is great because it gives a feeling of freedom and speed. Fast-paced environments favour, and benefit, from this approach to development.

However, there’s a not so nice side: JavaScript provides no support at all. You are responsible for figuring things out. For example, you can spend two hours coding and this makes you feel really productive, but you often need to go back and spend eight more hours fixing errors. There’s no way to proactively reduce errors while coding.

Types, on the other hand, force you to pay the price upfront: in my experience you spend eight hours writing everything and then only two hours making it work.

Because TypeScript requires more upfront work, we mistakenly assume that it’s unproductive compared to JavaScript. Upfront work saves us time fixing errors retroactively.

Each error that the TypeScript compiler catches saves you time to use on something else. For example, there’s a whole array of silly mistakes like typos, passing the wrong arguments, or forgetting to rename one occurrence of a variable during refactoring, that the compiler picks up for you.

How TypeScript helps with productivity

Types act as documentation

Whenever I write code, there’s always more code involved: Third party libraries, the language standard library, native APIs, and the rest of my application. In these cases, I’ve found the editor’s Go to definition feature to be a great friend, especially for third party code.

Having the ability to check how the code is implemented is a super power. Using Go to definition to explore the code allows me to quickly understand what the arguments of a function are, or what the relationship between functions in a library are. I find this quicker than reading the documentation.

Don’t get me wrong: Documentation is super important and has its purpose, but the additional ability to check the source and see things “under the hood” by myself saves me a lot of time – especially when it’s a library I am already familiar with.

Types reduce cognitive load

No matter how good you are at programming, you cannot keep everything in your head. The fewer unnecessary things we keep in our heads, the more space we have for important things. That’s why we use calendars, reminders, and to-do lists.

Working with a dynamic language like JavaScript means we have to keep track of a lot of information in our heads by ourselves. Keeping track of a lot of details from different files and functions is something our brain doesn’t do well. With static types, the compiler keeps track of that for us. By offloading all that information from your head and storing it in the compiler, you can focus on the important thing, which is the business logic of your code.

Using the compiler helps me to focus on my code. My “flow” during coding is a lot more efficient and streamlined with TypeScript than with JavaScript.

My normal flow when writing TypeScript, or any other typed language, goes like this:

  1. Make some changes
  2. Check the compiler
  3. If there are errors, fix them, else go back to step 1
  4. When I’m happy with the progress, run code

This works well for me even for UI development, as long as the changes aren’t visual.

On the other hand, with JavaScript my flow goes more like this:

  1. Make some changes
  2. Continue making changes, because I didn’t realise I made a mistake
  3. Run the code
  4. Try to figure out why it doesn’t work
  5. Some more debugging
  6. Alright, it was a silly mistake. Now back to step 1

In a sense, TypeScript feels like flying a plane with instruments to help you, whereas JavaScript is like flying only with your senses. With TypeScript, you still have to be a good pilot but you don’t have to keep track of a lot of mechanical information in your head - the instruments do it for you.

The more we leverage TypeScript features, the more we can focus on making our code do what it’s supposed to do .

Types scale

The compiler’s support pays off even more when making changes that span several files and modules. The compiler tells me all the places where I broke something instead of me having to remember all the usages of the function I changed or having to look it up.

Code is more of a living organism than something static that’s built once and never changes. As it grows and evolves, it becomes harder to get the full picture: More modules are added, and different parts of the code interact and affect each other. The chances of introducing bugs when we are making changes grow a lot. Because of the compiler, types give you confidence that changes aren’t breaking things.

With JavaScript, more often than not, I’m not even confident enough to make changes that span several files. The risk of introducing errors increases the more files one changes.

In other words, refactoring with TypeScript is more of a “when” question, whereas with JS it is more of a “can we afford it?” question - or even “are we crazy enough to do it?”

Being able to make changes faster with more confidence and fewer bugs equals productivity. However, using types doesn’t eliminate the need for testing; they go hand-in-hand.

Faster feedback cycle

To be productive, I have to be in the flow or in the zone. Once I get there, the longer I can stay, the better. Anything that enables a faster feedback cycle contributes to concentration and remaining focused. Because of the focus on compiling, TypeScript allows me to optimise for a faster feedback cycle and therefore stay in focus mode for longer.

Compiling code is usually faster than running it. When you run the code you have to get to the specific program state to be able to reproduce the part you are working on. And, to run we have to compile anyway!

tsc --watch --noEmit is your friend here, use it!

Depending on the editor you’re using, you will see the compiler errors inline - no need to even switch from the editor to the terminal.

Screenshot-2022-04-13-at-11.41.06

I know that compile times can be slow, especially TypeScript ones. If you put some effort into configuring things properly, like leveraging incremental compilation, then compiling will still be faster than running the tests or running the application. This means faster feedback and the ability to stay in focus mode for longer.

Types help you model your problems

Now, this is where things get interesting. There’s a lot we can do with TypeScript. If we use it only to type strings and numbers, we aren’t benefiting much from it. Yes, we still get nice errors when refactoring code, but why not take more advantage of it?

The real benefit comes when we use TypeScript to model our problem in such a way that we make impossible states impossible. That means we make it impossible to represent those states that, given the business logic of our programme, should be impossible.

This is easier to explain with an example. Say we are building a questionnaire application. We have a list of questions, and as the users answer them we get a list of answers. Answers can be null to allow users to skip some of the questions for later.

interface Questionnaire {
    questions: Array<string>
    answers: Array<string | null>
}

const state: Questionnaire = {
    questions: [
        'What is your favorite JS library?',
        'How many years of experience?',
        'Tabs or spaces?'
    ],
    answers: [
        'React',
        null,
        'Spaces, of course'
    ],
}

That looks good. But, what if our state ends up like this:

const state: Questionnaire = {
    questions: [
        'What is your favorite JS library?'
    ],
    answers: [
        'React',
        null,
        'Bananas'
    ],
}

You got that right, we have more answers than questions! And, who knows what the user answered Bananas for …

We can instead put the question and answer together, to make sure each question has only one potential answer, and we don’t have loose answers either.

interface Question {
    question: string
    answer?: string
}

interface Questionnaire {
    questions: Array<Question>
}

Now, we don’t want our questionnaire to have zero questions.

Here’s a neat trick to express non-empty arrays in the type system. This lets us be sure that our array isn’t empty.

interface NonEmptyArray<A> extends Array<A> { 0: A }

const foo: NonEmptyArray<number> = []  // Error, array is empty !!!
const bar: NonEmptyArray<number> = [1] // yay at least one element!

Think about the questionnaire example. If we went with the initial approach, it’s a matter of time that we end up introducing some bugs. We need plenty of tests to prevent it. With the second approach, there’s no way we have more questions than answers.

A big chunk of bugs were prevented at compile time. By doing this, you have more time to think about the problems you’re trying to solve.

The more you are able to think about the problems you’re trying to solve, the more likely you are to find the right models for them. And, the more those models leave out states in your program that shouldn’t happen, the higher the chances are that you’ll get things right faster - sometimes even at the first try.

This doesn’t mean you’ll always get everything right, but it does mean the code becomes more predictable. With more upfront thinking come more predictable results of what our code would do.

These are just a few examples. There are plenty of things we can do to make our programs safer and model our problems from the type level. For example:

If you’d like to learn more about Type Driven Development and Making impossible states impossible, you can read more here and here.

Note that applying these patterns takes practice. I brought these ideas from communities like Haskell and Elm that have a higher focus on correctness, but it all boils down to asking the question “How can I make it so that if my code compiles, then it works as it’s supposed to work”.

I encourage you to learn about other languages and bring new ideas that can make you a better developer on the language and framework you use everyday.

Learning and leveraging TypeScript

It’s not all fun and games: There’s always some price to pay.

In order to be productive in TypeScript, you have to learn it in depth. Don’t expect to start using it and be as productive as you are with JavaScript in the first week.

Although TypeScript is more strict, we can still be productive with it, and maybe even more so than with JavaScript. It might seem like you are unproductive because you have to fight with the compiler errors. Don’t look at the compiler as a foe, but rather as a pair programming partner - one that is really good at making sure we write correct code!

By leveraging its faster feedback cycles, reducing cognitive load by letting the compiler remember things for us and modelling our problems with types so it works if it compiles, you can be productive with the code you write.

If these examples convince you that TypeScript can boost your productivity and you’re interested in learning TypeScript, check out the resources below. And remember, as with anything, practice makes perfect!

Resources

If you are interested in learning TypeScript I recommend you get started with these:


Christian Gill codes in Haskell, PureScript, Elm, Reason and Rust, looking for ways to bring ideas and patterns from strongly typed functional programming into the mainstream. If you have something to share about that he'd be happy to hear!
Also, check out his dotfiles - every developer should have their dotfiles! :)

Recent posts

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.