TypeScript Types Explained – A Mental Model to Help You Think in Types

As a JavaScript developer, making the jump to TypeScript can feel intimidating at first. Suddenly you need to annotate your code with extra syntax, think about "types", and get familiar with concepts like "type inference". It may not be obvious at first why the extra effort is worth it.

In this post, I want to give you a mental model to understand how TypeScript‘s type system works under the hood. My goal is to go beyond just teaching you the syntax, and explain at a conceptual level what types are and how they can help you write better code. Hopefully by the end of this guide, thinking in types will feel like a natural way to approach JavaScript development.

What are type systems?

At their core, type systems are a way to classify the different kinds of values that can exist in a program. They provide a set of rules for what operations are allowed on each kind of data.

For example, a type system can specify that trying to call a string like a function should be a type error. Or that passing a number to a function that expects a boolean is not allowed.

Having a type system helps catch bugs early in the development process, during code compilation, rather than at runtime when the program is executing. It‘s like having guardrails that keep you from making certain kinds of mistakes.

Type systems also act as a form of documentation. By looking at the types used in a codebase, you can quickly get a high-level understanding of what kinds of data are flowing through the program.

Static vs dynamic typing

Programming languages are often classified as having either static or dynamic type systems:

  • In a statically typed language, type checking happens at compile time. The compiler analyzes the types in the code and flags any type errors before the program even runs. Examples include Java, C++, and Rust.

  • In a dynamically typed language, type checking happens at runtime. The interpreter does its best to run the code, even if the types don‘t always match up. It will raise type errors if something goes wrong while executing the program. JavaScript, Python, and Ruby are dynamically typed.

Static typing proponents argue that it leads to more reliable and maintainable code, since type errors are caught early. Dynamic typing fans counter that it allows for faster development and more flexibility.

Gradual typing and TypeScript

TypeScript offers a middle ground between the two approaches, known as gradual typing. The idea is that you can incrementally add type annotations to a codebase to get increased static type safety, while still keeping the flexibility of JavaScript.

With TypeScript, you‘re not required to annotate every variable and function with types. The compiler does its best to infer types where it can. For example, if you write const x = 42, TypeScript will infer that x is of type number without you having to explicitly say so.

You can also mix and match typed and untyped code. If you‘re working with a 3rd party library that doesn‘t have type definitions, you can still use it from TypeScript by declaring the types as any (which turns off type checking for that value).

This gradual approach allows teams to progressively migrate an existing JavaScript codebase to TypeScript, while still getting immediate benefits from the parts that are typed.

Benefits of TypeScript types

So why go through the trouble of adding type annotations to your code? Here are a few key benefits:

  1. Early bug detection. TypeScript can catch type-related errors at compile time, preventing bugs from making it to runtime.

  2. Better tooling. IDE‘s and editors can use type information to provide better autocomplete, code navigation, and refactoring tools.

  3. Self-documenting code. Type annotations act as a form of documentation, making it easier for developers to understand how a piece of code is supposed to work.

  4. Safer refactoring. When you change the type of an API, the TypeScript compiler will flag any places in the codebase that need to be updated.

  5. Improved team communication. Agreeing on types forces teams to think through the design of their APIs and can catch misunderstandings early.

Basic TypeScript types

TypeScript supports all the standard JavaScript types, plus a few extras. Here are the most common ones you‘ll encounter:

  • string: represents text values like "hello" or ‘world‘
  • number: represents numeric values like 42 or 3.14
  • boolean: represents true or false
  • array: represents lists of values like [1, 2, 3]
  • object: represents dictionaries of values like { name: "Alice", age: 30 }
  • function: represents callable code like () => { console.log("hi"); }

You can specify the type of a variable or function parameter by adding a colon and the type after the name:

let name: string = "Alice";
let age: number = 30;
let isStudent: boolean = true;

function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

If you don‘t specify a type, TypeScript will do its best to infer one based on how the value is used.

Advanced types

Beyond the basic primitive types, TypeScript supports a number of more advanced type concepts:

  • Union types: these let you specify that a value can be one of several types. For example, let myValue: string | number means that myValue can be either a string or a number.

  • Intersection types: these let you combine multiple types into one. For example, let myValue: { name: string } & { age: number } means that myValue must have both a name property of type string and an age property of type number.

  • Generics: these let you write code that is reusable across different types. For example, you can define a generic Array<T> type that works for arrays of any type T.

  • Type aliases: these let you give a name to a type so you can reuse it across your codebase. For example, type User = { name: string, age: number }.

  • Interfaces: similar to type aliases, interfaces let you define the shape that an object must have. For example, interface User { name: string; age: number; }.

These advanced types let you model your data in a more precise way and catch even more potential errors at compile time.

Structural typing

One important thing to understand about TypeScript‘s type system is that it is structural, not nominal.

In a nominal type system (used by languages like Java and C#), two types are only considered compatible if they have the same name. For example, if you have a Person class and an Employee class, you cannot use an Employee where a Person is expected even if Employee has all the same properties as Person.

In a structural type system like TypeScript‘s, two types are considered compatible if they have the same shape—that is, if they have the same properties with compatible types. The names of the types don‘t matter.

So in TypeScript, if you have an object with a name string property and an age number property, it is considered compatible with a Person type that also has name and age properties, regardless of what the object‘s actual type is called.

This structural typing can take some getting used to if you‘re coming from a nominal typed language, but it offers more flexibility in how you can structure your code.

Designing good types

To get the most benefit out of TypeScript, it‘s important to design your types carefully. Here are a few best practices to keep in mind:

  1. Be as specific as possible. The more precise your types are, the more errors you can catch at compile time. Avoid using any unless absolutely necessary.

  2. Use union types for parameters that can accept multiple types. For example, a formatDate function that can take either a Date object or a ISO 8601 string.

  3. Use type aliases and interfaces to give readable names to commonly used types in your codebase.

  4. Don‘t go overboard with type annotations. Let TypeScript infer types where it can, and only add explicit annotations where needed for clarity or to catch potential bugs.

  5. Keep your types focused on the public API of your code. Don‘t expose implementation details through types unless needed.

  6. Use comments to document any type-level assumptions or invariants that cannot be expressed through the type system itself.

Runtime vs compile time

It‘s important to remember that TypeScript is a compile-time only tool. The type annotations and checks are all erased when your code is compiled to JavaScript.

This means that TypeScript cannot catch all possible runtime errors. For example, it can‘t prevent you from accessing a property on an object that doesn‘t exist, or from calling a function with the wrong number of arguments.

The goal of TypeScript is not to completely eliminate runtime errors, but to catch as many type-related errors as possible before your code even runs. Runtime checks are still needed for things like validating user input or handling unexpected API responses.

Conclusion

I hope this post has given you a better understanding of how TypeScript‘s type system works and why adding types to your JavaScript code can be so useful.

The key concepts to keep in mind are:

  • Types provide a way to classify the different kinds of values in your program and specify what operations are allowed on them.
  • TypeScript‘s gradual typing approach lets you progressively add types to a codebase, while still keeping the flexibility of JavaScript.
  • TypeScript uses structural typing, meaning that types are compatible if they have the same shape, not just the same name.
  • Designing good types requires finding a balance between specificity and simplicity. The goal is to catch as many errors as possible while still keeping the code readable and maintainable.
  • TypeScript is a compile-time only tool. It cannot prevent all possible runtime errors, but it can catch many type-related issues before the code even runs.

Adding types to a codebase takes some extra effort, but the benefits in terms of reliability, maintainability, and developer productivity are well worth it in my experience. Once you start thinking in types, it‘s hard to go back to vanilla JavaScript!

Resources to learn more

If you want to dive deeper into type theory and TypeScript, here are a few resources I recommend:

I hope this guide has been useful in understanding TypeScript‘s type system. Happy typing!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *