How I Started Writing TypeScript

The first time I tried writing TypeScript, it didn’t stick.

After two hours, my main takeaways were:

  1. This makes my code harder to write
  2. I could accomplish this with vanilla JS much faster
  3. Everything easy is hard again

Ultimately, I left with the impression that TypeScript was another tool I didn’t need.

Fast forward 12 months and my opinion has completely changed. TypeScript is the most valuable thing I’ve learned all year. If you’ve been on the fence about whether to embrace TypeScript, I’d encourage you to give it a try.

What is TypeScript?

TypeScript is a superset of JavaScript. That means it contains all the features of JavaScript and compiles to JS, but has additional features and syntactical sugar sprinkled on top. The main feature is optional static typing – more on that later.

TypeScript is an open-source project developed and maintained by Microsoft. That means it is widely supported and integrates magically with many IDEs (especially VSCode).

The official TypeScript docs describe TypeScript as “JavaScript that scales”. And it really does!

Why do I need TypeScript?

Well… you don’t. But you might want it if:

  1. You frequently work with other engineers in a shared codebase.
  2. You’ve ever made a “small change” to a piece of code only to discover later that the change unintentionally broke other parts of the codebase – parts you didn’t think were related in any way.
  3. You don’t feel like you can make changes to your codebase with confidence. As a result, you frequently spend hours visually QAing your work to make sure nothing broke.
  4. Your project has a component-based architecture (React, Vue, etc) and you can never remember which components take which props.
  5. You’re willing to invest a little extra effort up front in exchange for enjoying a more pleasant, sane, and productive programming experience as your project grows.

Why static types?

Many human problems are the result miscommunication. One person expects another person to perform a task in a certain way and becomes frustrated when the task is not completed in accordance with their expectations. It’s unlikely that either party intends harm, but a lack of clearly defined and mutually agreed-upon expectations leads to unexpected results.

This same scenario happens in code all the time: a function requires specific data to perform correctly; everything crashes when its expectations are not met.

Static types largely solve this problem. When expectations are aligned with reality, writing code becomes a much more pleasant and predictable experience.

TypeScript in action

What does it look like? How does it work? Let’s dive into some code.

Say we have a vanilla JS function that logs a message announcing today’s lunch. It has one parameter called mainEntree:

export function logTodaysLunch(mainEntree) {
  const lunchMessage = `Today's lunch is ${mainEntree}. Yum!`
  console.log(lunchMessage)
}
logTodaysLunch("Pizza")
// Console: Today's lunch is Pizza

It’s fairly obvious from reading the code above that the mainEntree parameter should be a string. But nothing is stopping us from calling logTodaysLunch with a number as the argument like this:

logTodaysLunch(197)
// Console: Today's lunch is 197

The code above would not cause any errors because it is technically valid. But it would result in a message that reads “Today’s lunch is 197”, which is not what we intended. Most people don’t like to eat numbers for lunch.

This is precisely the problem TypeScript and static types solve.

With TypeScript, we can assign mainEntree a type of string:

export function logTodaysLunch(mainEntree: string) {
  const lunchMessage = `Today's lunch is ${mainEntree}. Yum!`
  console.log(lunchMessage)
}

Which will result in this helpful error message if logTodaysLunch is called with a number:

logTodaysLunch(197)
// Compiler error: Argument of type '197'
// is not assignable to parameter of type 'string'

Nice catch! TypeScript just caught a bug automatically.

But what if we try to call logTodaysLunch with a nonsensical string? Can we trick TypeScript?

logTodaysLunch("Cybertruck")
// Console: Today's lunch is Cybertruck

No error. Hmm… not ideal. Cybertruck does not taste good either.

Luckily, TypeScript will allow us to create a custom type:

type LunchType = "sandwich" | "soup" | "salad"
export function logTodaysLunch(mainEntree: LunchType) {
  const lunchMessage = `Today's lunch is ${mainEntree}. Yum!`
  console.log(lunchMessage)
}

This time, TypeScript catches the error and provides a helpful message:

logTodaysLunch("Cybertruck")
// Console: Argument of type "Cybertruck" is not assignable
// to parameter of type LunchType

Now we know logTodaysLunch is expecting an argument that matches one of the values we defined in LunchType. It will only accept sandwich, soup, or salad. Another edge case eliminated!

IDE integrations

TypeScript empowers many IDEs to provide contextual information that can help you be more productive. VSCode’s IntelliSense feels a lot like a superpower.

All of the examples above were easy to follow because we called logTodaysLunch in the same file immediately after it was defined. The reality is that this function could be imported from a helper file like this:

import { logTodaysLunch } from "lunchHelpers"
logTodaysLunch(????) // We don't know what arg to pass anymore :(

Without the function definition directly above to provide context, we no longer know what argument(s) to pass to logTodaysLunch. Normally, we’d have to track down the lunchHelpers file and read through everything.

Not with TypeScript. If you were to hover over logTodaysLunch in VSCode, you’d see the following IntelliSense tooltip:

(function) logTodaysLunch(mainEntree: LunchType)

We immediately know that logTodaysLunch takes one argument called mainEntree that has a type of LunchType. This contextual information is especially helpful for a new engineer who has never used the logTodaysLunch function before. Plus, we can proceed with confidence knowing TypeScript will catch any errors if an incorrect value is passed.

When to use TypeScript

Since TypeScript requires a compile step, it does require a bit more setup time than regular JavaScript.

For future projects, I plan to:

  • Always use TypeScript to build anything that qualifies as an “app”. TypeScript is most helpful for complex projects.
  • Evaluate everything else on a case-by-case basis. If I can get the job done with 20 lines of vanilla JS or jQuery, I see no reason to add additional complexity and install TypeScript.

Making the investment

The big questions to ask when learning a new technology are:

  1. Will the time investment required to learn this new tech pay off in terms of increased productivity, better code quality, or developer happiness?
  2. Will this new technology still be around in 5 to 10 years?

For TypeScript, the answer to both is a resounding YES.

If you’ve been burned in the past by investing time into a dead technology, you understandably feel some hesitation. JS frameworks and flavors come and go, but TypeScript is something entirely different. All JavaScript code is valid TypeScript code. You’re already writing TypeScript! All you need to do now is add types.

TypeScript’s ultimate goal isn’t to make your code look prettier or mimic the trendiest language du jour. TypeScript is here to help you write better, more maintainable JavaScript.

Start writing TypeScript. Start writing code that scales.