In this post, we’ll dive straight into learning the ins and outs of TypeScript: why you’d use it, how to write it, how it works under the hood, and what types are available. In later posts, we’ll review TypeScript use cases and advice and explain how to integrate TypeScript with popular libraries we use here at HubSpot, namely React.
What is TypeScript?
Some of the built-in types may look familiar — boolean, string, number, undefined, and null, for example — though you can also define your own types as interfaces or types. When you assign a type to a symbol, TypeScript's language services get to work — your code will automatically be validated against that type.
The type-checking process
TypeScript provides type safety, helping you catch errors within your IDE, long before you run a site in a browser or your code hits production. By adding types, you are encouraged to be more declarative about your code's intentions. Add the language service and autocomplete features to the mix, and you've got yourself automatic in-IDE documentation. This is particularly helpful if you're writing a library that other consumers use.
Let's take the example below. What would you expect to be logged?
This underline hints that TypeScript has flagged a potential issue. Hover over it, and you'll see a suggestion about adding a parameter type — right now, it's automatically set to any, a catch-all type that you should try to avoid using as it forfeits type safety. TypeScript is suggesting you add a type here.
By adding a type, you are stating your expectations explicitly: I'm inputting an object of type number, expecting to output an object of type number. If there are any issues, or if I overlook anything, let me know while I'm coding in my IDE, long before this code is in production.
How to assign types
To assign a type, add a colon (:) after an object's name and specify its type:
How to declare a value's type
So far, we've covered the boolean, string, number, undefined, and null types. We've also mentioned any, a type you should avoid using as it forfeits type checking and helpful tooling features, as we'll see throughout this post. To declare the types of values an array should hold, you can either use the Array<T> syntax or the T syntax, where T is the type of values in your array. The Array<T> syntax is a generic type — we'll discuss generics later.
The two forms of syntax are functionally identical, so use whichever one your team prefers. The T syntax is a shorthand for the generic form (Array<T>).
To type an array, you might be wondering if you could place the type inside of the brackets (for example, [string]). Unfortunately, that would be a very different type! When types are placed within the  brackets, that signifies an array of fixed length, also known as a tuple.
The type [string] would mean an array of length 1 filled with a single string. For example, ['a'] would be valid, while ['a', 'b'] would not.
With tuples, you may mix types as needed:
You'll often find tuples in libraries that rely on array destructuring. For example, a React useState call, which returns an array of length 2 with 2 different types:
Use the tooling by hovering over each item to get its type. Most libraries, like React, will expose types (for example, React.Dispatch and React.SetStateAction, as seen above) which you can import and use directly:
To explore the library's types even further, right-click a symbol and click 'Go To Definition'. This is a great way to peek behind the curtain of TypeScript's language service to see what DOM and other types are defined, as well as how they are written. When typing DOM elements, functions, and the like, try to use the 'Go To Definition' menu as much as possible to learn best typing practices.
When typing functions, you'll need to keep your eye on defining two types: the parameter and the return types. In the buggy increment example we saw earlier, we want our parameter type to be a number and our return type to be a number, too. Parameter types can be added next to each parameter, while return types can be added after the parentheses, as shown below:
Functions that don't return anything (for instance, a function that logs to the console or calls another function) should have a return type of void.
Functions that throw exceptions or fail to terminate should have a never type — the never set of values represents an empty set, or values which can never be encountered.
Most TypeScript developers recommend adding a return type whenever possible. One notable exception is when your function returns a value with a type that should be inferred from another library, as this allows the type to change if and when the library's return type changes, and will cause TypeScript to flag mistakes in your implementation instead.
Generics allow you to create type declarations that capture the types of input a user provides to further provide type-checking based on those types. This allows you type-check based on a user's input in a reusable DRY format. Generics are characterized by the <> brackets, which contain a reference to the captured type used for later type-checking.
For example, let's say we have a function that takes an argument and returns an argument of that type:
Although we could use any as the parameter and return types, that's rarely, if ever, a good idea. You would miss all mixed-type cases (for example, inputting a string could unexpectedly return a number, and that case would never be flagged). Using any opts you out of type-checking entirely, meaning you won't get access to TypeScript's helpful language services, intelligent autocomplete, or the ability to rename symbols, in many cases. In a function context, it also breaks the contract of having an expected input and output, potentially making your implementations less reliable.
Instead, what we need is to automatically capture the input type to determine what the matching output type should be. We need a generic!
To establish a generic in this case, add <T> to capture the type that's passed in. Then, while typing your parameter and return types, you can reference that T type. The T symbol is commonly used since it stands for Type, though you could use any other keyword that makes sense in your codebase (for example, ArgsType or Props).
Now that we've leveraged generics to type our parameter and return types, we can take advantage of TypeScript's language services to get helpful features like autocomplete:
Once you start working with TypeScript, you'll see generics everywhere. In the below video, we'll walk through this generic function example and then talk about how to explore generic types you'll run into elsewhere, such as the generic type Array<T> from the TypeScript-provided DOM declarations:
In most cases, TypeScript will infer the type from the input. This is called type argument inference. However, in certain more complex cases, you may need to set a type explicitly. Oftentimes this will happen with functions like the map<T> array helper and React's useState<T>. When needed, hover over the function to ensure it takes a generic type, and then set a type explicitly:
Interfaces and type aliases
In TypeScript, any object can be represented by two different types: interfaces and type aliases. The syntax for these two types is slightly different:
|Interface syntax||Type syntax|
The biggest difference between interfaces and types is that interfaces are augmentable. For example, you might be writing a library and want consumers to be able to extend an interface to suit their needs via a process known as declaration merging:
|Merged interface declaration||Unmergeable type alias|
Both can be used as a basis for composing additional types. Interfaces can be extended with the extends keyword while types can be extended with the & symbol (or, depending on your needs, the | union symbol we saw previously). If you think of these two types in a set theory sense, extends means "subset of" while & means "intersection" (or A ∩ B, the set composed of all elements that belong to both A and B), which explains some interesting results:
Which to use largely depends on the scenario. If you're publishing type declarations for an API or shared library, it might be useful for consumers to be able to merge in new fields if and when the API or library changes, so use an interface. If you're using a type declaration in an internal project only, you may want to avoid declaration merging, so use a type alias. When composing new types, extending interfaces is more performant than intersecting types, so prefer using interfaces as discussed in the TypeScript wiki.
Last but not least, TypeScript includes another feature you may be familiar with from other languages: Enums.
Since enums are a TypeScript-specific construct. They allow a developer to define a set of named constants, as seen above and in the official TypeScript docs.
Recommendations for your first TypeScript project
Although there is a
tsconfig.json compiler option (
That's it! I hope you enjoyed. Looking for more before our next TypeScript posts drop?
- Execute Program's Everyday TypeScript course: If you're struggling with TypeScript syntax and types, try this course. It's broken into bite-size interactive coding examples which reinforce concepts through repetition.
- DevHints.io's TypeScript cheat sheet
- Effective TypeScript: This book is jam-packed with tangible examples and recommendations. Use it to develop a more nuanced understanding of TypeScript.
- React TypeScript Cheat Sheet: Even if you're not a React developer, you'll likely find this handy cheat sheet helpful. The docs are full of practical examples for things like event handler types and DOM element types.
Want to work on a team that's just as invested in how you work as what you're working on? Check out our open positions and apply.