What it is, why you should use it, and how to spread the good news to the rest of your team.

Recently, the Full Stack engineering team at Hopjump has added Typescript to their toolset. This blog post was adapted from a talk that was given internally to introduce the language to the rest of the engineers.

What is Typescript?

Compiles to Javascript

Typescript is a compile-to-Javascript language created by Microsoft. Since Javascript is (currently) the de-facto “runtime” for the web, developers must write their web code in Javascript. If you want to write a language other than Javascript, it must compile to Javascript much like C compiles to machine code or Java compiles to Java bytecode.

Typescript isn’t the only language that compiles to Javascript. Other examples include Coffeescript, Dart, or even some experimental compilers for more traditional languages like Ruby or Python.

Another interesting property of Typescript is that it is a super-set of Javascript, meaning that all valid Javascript is valid Typescript as well. This is a great thing – for reasons I’ll outline later.

Compiling Javascript isn’t an unusual thing these days, either. With build tools like Webpack or Parcel, most web developers are already compiling or transpiling their Javascript so they can use modern syntax, compile-to-Javascript languages, or minify their production Javascript.

Statically-Typed Language

What makes Typescript different than most other compile-to-Javascript languages is that it is statically typed. Statically typed languages are languages where a variable’s “type”, such as number, string, or object, is known before the code is actually run. This is useful because it can prevent type-related bugs, and provide superior developer tooling.

Some other examples of statically typed languages are Java, C/C++, and Go.

Typescript’s type system is very unique because it’s built specifically for Javascript. Javascript is a very dynamic language, which means the type system that Typescript provides is very dynamic as well.

For instance, you may see this kind of code in Javascript:

function generateRandom(type) {
  if (type === "string") {
    return generateRandomString()
  }
  if (type === "number") {
    return Math.random()
  }
  if (type === "boolean") {
    return Math.floor(Math.random() * 2) === 0
  }
  return null
}

This function is very dynamic and returns different types depending on how it’s called at runtime. How would you add static types to a function like this? Typescript is dynamic enough to allow for this kind of thing, whereas a more strict statically typed language might not.

Why Typescript?

Before we jump into how to read and write Typescript, let’s briefly talk about the motivation behind it.

Safety

Unfortunately, a lot of bugs are due to type errors. Consider this code:

let hotelId = 123

// If the hotel ID ends with 7...
if (/^\d*7$/.test(hotelId)) {
  console.log("You picked a lucky hotel!")
}

Can you spot the bug?

The bug is that we’re checking if the hotelId matches a regex, but you can only match a regex against a string. We can fix that by adding a .toString() method call before passing it into the .test method.

let hotelId = 123

// If the hotel ID ends with 7...
if (/^\d*7$/.test(hotelId.toString())) {
  console.log("You picked a lucky hotel!")
}

Now, we either caught that bug at runtime (we ran the code, and it blew up), or our brain caught it by reading the code. While we could rely on our brain to catch all of these type bugs, this is the perfect type of problem that Typescript was created to solve. It can catch this problem and expose it to you via a compiler error or red underline in your editor.

Screenshot is of VSCode with Night Owl theme

Typescript can also prevent things like typos if you’re working with plain strings. From our example earlier:

generateRandom("strang")

This should result in a type error because we misspelled “string” as “strang”. Typescript can catch this type of problem for us too.

Automatic Documentation

Have you ever forgotten how to use a library and had to look it up every time you use it? Even if you wrote that library? I constantly have to pull up docs or code to see what a function takes while I’m calling it, so that I can be sure I’m calling it correctly.

With Typescript, you can avoid looking up the docs in some situations because your editor will tell you how to call it directly. This is even more powerful with Intellisense in a tool like VSCode (recommended editor at Hopjump).

Automatic documentation with Intellisense is a lifesaver

Easier Refactoring

I find it somewhat scary to refactor a function in Javascript. If I add a parameter to a function, it’s hard to tell if I found and updated every caller of that function to use the new signature. This is where Typescript shines.

Say I have this function:

function add(a, b) {
  return a + b
}

add(1, 2)

If I wanted to add 3 numbers instead, I would rewrite this function like so:

function add(a, b, c) {
  return a + b + c
}

add(1, 2) // returns NaN

I would have to search the project for add, which may be difficult depending on how widely used that function is. Finding all add(...) function calls may be trivial, but what about if we’re passing around the add function directly? It becomes much harder to track down.

Typescript can help with this by underlining the errors when you change a function’s signature.

Most popular editors can display Typescript errors inline

If you’ve ever used a language like Java, popular IDEs like IntelliJ IDEA or Eclipse will provide advanced refactoring tools. Because Typescript is statically typed, it enables some of these in VSCode too.

How do I write Typescript?

Okay, onto the fun part – what does Typescript look like?

Basic Types

Typescript has a handful of default types. Here are examples of the most common ones:

string // as you’d expect - “hello world”
number // 123 or 1.23 or even hex 0x123
boolean // true or false
string[] // array of strings. Also aliased: Array<string>
object // {} or { hello: “world” } - usually there’s a better option though
any // Any variable fits this type. Basically, “give up on type checking”

You can annotate variables and function arguments/return types with types:

const hotelId: string = “123”

function add(a: number, b: number): number {
  // ...
}

Typescript also has the concept of union types, which are created with |. For instance, perhaps you want to adapt the add function to take strings or numbers:

function add(a: number | string, b: number | string): number | string {
  // ...
}

This means that you can call add(1, 2) or add(“hello”, “world”), but not add(true, {}). A union type a | b will allow type a or type b.

Since these types can get pretty gnarly, you can also add your own custom type aliases with the type keyword.

type Addable = number | string

Now, you can use that type alias in variable or function declarations:

function add(a: Addable, b: Addable): Addable {
  // ...
}

You can even export those types like you export other Javascript variables, by prefixing it with export!

export type Addable = number | string

Custom Interfaces

Say we wanted to create an object that represents a hotel:

const hotel: object = {
  name: “Boston Park Plaza Hotel”,
  id: 123,
  starRating: 4,
  amenities: [
    { id: 1, name: “Pool” },
    { id: 2, name: “Internet” },
  ],
}

If you tried to use this object, quickly you’ll run into issues:

The object type doesn't provide great Intellisense suggestions

Note that Intellisense isn’t helping me at all here; all of the suggestions are words that I typed in the file.

In reality it doesn’t really make sense to use the object type here, because it doesn’t tell you anything about the structure of the object. Luckily Typescript has a very easy way to create objects that match a certain structure – interfaces. Here’s how that looks:

interface Hotel {
  name: string
  id: number
  starRating: number
}

Now, we can use Hotel instead of object.

const hotel: Hotel = { ... }

However, I left out the amenities field. How would we type that? Well, it makes sense to create an interface for Amenity, too:

interface Amenity {
  id: number
  name: string
}

Now, we can use the Amenity interface in our Hotel interface using the array syntax from earlier:

interface Hotel {
  name: string
  id: number
  starRating: number
  amenities: Array<Amenity> // or Amenity[]
}

Now, Intellisense is much more useful for the hotel object.

When the object is properly typed, Intellisense displays all possible methods

Notice that we didn’t have to “instantiate” a new Hotel model or anything like that. That’s because Typescript uses duck typing to type things. This means that if an object has all of the fields a Hotel needs, it’s a Hotel. This works well with how Javascript models things.

Like before, you can export interfaces as well as types so that they can be used in other files.

Usage with React

Historically Hopjump has had prop type checking using the package prop-types. This package allows you to “type” your props and an error will be printed at runtime if the component was instantiated with the wrong props. This worked fairly well for us.

const HotelCard = ({ hotel }) => {
  return (
    <div>
      <h1>{hotel.name}</h1>
      {hotel.starRating} Stars
    </div>
  )
}

HotelCard.propTypes = {
  hotel: PropTypes.shape({
    name: PropTypes.string.isRequired,
    starRating: PropTypes.number.isRequired,
  }).isRequired,
}

At runtime, if you instantiated the HotelCard component without a properly formatted hotel, it would log an error. However, this didn’t help us at all when writing components that instantiated this component. For instance:

const HotelList = () => {
  return (
    <div>
      <HotelCard />
    </div>
  )
}

Our editor would report no errors with this, but when we run the code we would get a runtime exception. This led to some bugs that weren’t discovered until we deployed to production if it was on a page that we didn’t have proper test coverage on.

Thankfully Typescript solves this for us. React exports types that we can use in our components.

const HotelCard: React.FunctionalComponent = ({ hotel }) => {
  return ... 
}

// Or: React.FC as shorthand:
const HotelCard: React.FC = ({ hotel }) => {
  return ...
}

This tells Typescript that we are creating a functional React component, and we get some stuff for free like the children and key prop. Immediately, Typescript gives us an error about the hotel prop, since it doesn’t know about it.

Typescript React interfaces provide useful, actionable error messages

This is because we didn’t specify that we were expecting a hotel prop. We can fix that by converting our PropTypes to a Typescript interface.

interface HotelCardProps {
  hotel: Hotel // using our Hotel interface from earlier
}

We also have to tell the component that this interface should be applied to the component’s props.

const HotelCard: React.FC<HotelCardProps> = ({ hotel }) => {
  return ...
}

Now, Typescript knows the type of the hotel prop passed into the component. We also get errors in HotelList where we called it without a hotel prop:

Typescript allows us to catch prop errors statically

Perfect! We’ve moved a runtime error into a statically typed error, which should reduce the time spent debugging.

Note: I’ve only shown how to type functional components, but class components can be typed too!

Statically-typed React Hooks

React hooks can also be statically typed. After all, they’re just functions!

const Counter = () => {
  const [count, setCount] = useState(0)
  ...
}

Notice if you hover over the count variable, your editor should be able to figure out that count is of type number. This is because Typescript’s type inference is very good and it can tell based on the type of the variable you passed into useState. This won’t work if you don’t pass in a variable, or if you pass something like null or undefined in because those can’t be typed. In that case, you can tell Typescript specifically the type you’re expecting:

const [hotel, setHotel] = useState<Hotel>(null)

If you’re creating your own hook, such as useCounter, you can type that exactly like you’d type a function:

function useCounter(initial: number): [number, () => void] {
  const [counter, setCounter] = useState(initial)
  return [counter, () => setCounter(i => i + 1)]
}

A couple Typescript things that might be new here:

  1. The return type of this function is [number, () => void]. This means it returns a tuple of length 2, with the first thing being a number and the second a function. This is common with hooks (useState returns a tuple, for instance).
  2. The function returned is of type () => void. Typescript doesn’t give us a function type because, like object, it would be pretty useless. Instead, a function is defined by its argument and return types, or in this case 0 arguments and a void return type. This means this function doesn’t return anything.

Now, when you use useCounter in your component, it will be properly typed!

I’m convinced. How do I use Typescript in my own projects/at my company?

Great! Now that you’ve seen how powerful Typescript can be, it’s time to introduce it to your own app.

Convincing others to use Typescript

I’ve found that one of the biggest barriers to adding a new technology, especially in a corporate or office setting, is convincing others to use it. It may be a great technology, but unless you can sell it to others it doesn’t matter. Perhaps you can put together a presentation, or show your coworkers this blog post.

Adding it to your App

The first step to add it to an app is getting it setup with whatever framework you’re using, if you’re using one. If it’s create-react-app, then you can follow the instructions that create-react-app provides. If it’s Next.JS, then follow Next’s instructions. If you have an app without an underlying framework, you can add a Typescript integration to your Webpack, Parcel, Rollup, or whatever bundler you have. And if you don’t have a bundler, you can run tsc (the Typescript compiler) manually.

After Typescript is initially setup, it’s very easy to introduce to an app. Earlier I mentioned that Typescript is a super-set of Javascript. This is a great property because it means you can rename a file from file.js to file.ts (or file.jsx to file.tsx if using JSX) and Typescript is able to compile it without any syntax changes.

By default, any types that Typescript cannot figure out automatically get the any type, which is very lenient. You can specify better types to get better type coverage, but it can be done incrementally.

Don’t be scared to use the any type at first. Better types can always be added later.

Also, don’t force everybody to write Typescript right away. It’s better to incrementally add Typescript until everybody working on the app feels comfortable with it. At Hopjump, we’ve incentivized developers to write Typescript by giving them a Typescript sticker after they commit their first Typescript file.

Sticker motivation to persuade stubborn developers

Good luck, and spread the good news of Typescript to others!