TypeScript 102 -  Beyond The Basics
Tags: webdev, typescript, beginners, tutorial
Published: 2023-08-23 12:47:41


![Beyond The Basics Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxltaqum8jx6qaoqp4l0.png)

The seemingly endless sea of TypeScript tutorials you'll find online do a great job of providing resources to both beginners and advanced users, but what about those in the middle? If you know the basics but aren't quite at the advanced level yet, then this series of articles is for you. In it we'll be exploring some tools and techniques that'll take your TypeScript code to the next level!

This guide assumes that you're already familiar with the basics of TypeScript and JavaScript. You should understand the concepts of types, interfaces, the `as` keyword, functions, and classes, as we'll be building on all of these fundamentals.

Let's start in the shallow end: the building blocks TypeScript gives us and what we can do with them.

## Union (|) and Intersection (&) Types

![Union and Intersection Venn Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51orjdx7nynogtulr7tm.png)

Unions and intersection types are incredibly useful for enhancing the flexibility of your code.

A union type, denoted by `|`, allows a variable to be one of several types. This is useful when we want our code to accommodate a variety of data types. Here's an example using interfaces:

{% gist https://gist.github.com/Max-M-Feige/402767702e0e0232f9172352a50faad2 %}

In this case, a person is either a Teacher or a Coder. The `work` method, which is shared by Teacher and Coder, is valid since it's present regardless of which type person is. Methods specific to one interface, however, may not exist in person, and that's where TypeScript steps in to stop us from trying to use them. Without additional work, TypeScript will only allow us to use properties that it knows will exist.

We can also use unions with primitive types:

{% gist https://gist.github.com/Max-M-Feige/6a1da8b81466f8a9be94f1137a9237fe %}

In this example, we're allowing `myVar` to be a `number` or a `string`. TypeScript ensures we can only use properties or methods common to all types in the union, thus preventing us from using the string-only method `toLowerCase`.

When we use a union, we're establishing that any variable assigned to our type is going to satisfy the properties of at least one of the provided types. TypeScript will then go further and only allow us to access properties and methods common to all types - at least if we don't give TypeScript more information to narrow the type with! Don't worry if that sounds complicated, we'll dive into narrowing shortly.

Next up is the intersection! Intersections, denoted by `&`, complement the union and allow us to merge multiple types into one. A variable declared using an intersection must have the features of all combined types. Let's illustrate this with an example:

{% gist https://gist.github.com/Max-M-Feige/d29d21c88ff4aafbb2dd4c1846b6468f %}

As you can see, an intersection allows us to create a new type by combining the overlapping properties of both interfaces. Intersection types can be used with primitives as well, as shown here:
{% gist https://gist.github.com/Max-M-Feige/5b585ee4a09129cef03902bea0d98356 %}

Since the primitive types `string` and `number` are mutually exclusive, an intersection is impossible. The intersection results in the type `never`, which we'll talk more about soon. For now, just know that it symbolizes a type that can't exist.

If we try to intersect two types with conflicting properties we're also greeted by `never`:

{% gist https://gist.github.com/Max-M-Feige/9866b759ec4f9ed60a1ae1e2a66b7d5f %}

In this example intersection, `id` would be required to be both number and string simultaneously, which is impossible. TypeScript sees this and assigns it the type `never`, effectively preventing us from using our newly created type.

Overall, intersections allow us to construct complex types from simple building blocks, and TypeScript will ensure that any values we assign to an intersection satisfy all types in the intersection.

Utilizing both unions and intersections effectively gives you the ability to easily mix and match your existing types with ease, saving you the effort of manually creating similar types.

## Type Narrowing
One of TypeScript's most important features is type narrowing, a process by which the type of a variable is (wait for it) narrowed using, for example, a conditional. This gives TypeScript more information about what type we're providing it. Let's expand on a previous example to see this in action:

{% gist https://gist.github.com/Max-M-Feige/366828453b10d924a3e04f5c1bf524f0 %}

Our function `specialPrint` takes an item, which is either a string or a number. The function will then check if item is a string and will use either `toLowerCase` or `toFixed` based on the result. Thanks to the conditionals the methods are contained in, TypeScript will recognize that the code can only reach these methods when item is the correct type and will bypass its limitations on methods that aren't shared between every type in the union.

Ever have a variable in limbo waiting for user input or for a network request to finish? These variables normally start out as undefined, which can be a common source of headaches for many JavaScript developers. TypeScript gives us the tools to tackle this issue by using a combination of unions and type narrowing. This approach helps us write code that's clean and bug-free!

{% gist https://gist.github.com/Max-M-Feige/5e1f87fa25511ae4264d6b55105d46e2 %}

In this example, any Person could be a `Teacher`, `Coder`, or `null`. Since `null` has no methods, we cannot directly call `person.work()`. Instead, we must ensure that person is not `null` with a simple conditional. Once we've done this, TypeScript recognizes that person is type `Teacher | Coder` and we're able to call the `work` method safely.

This is where the power of type narrowing really shines. By using type checks, we can ensure that our code behaves as expected and is free of runtime errors. This makes our code safer, easier to understand, and easier to maintain. There are many more ways we can employ type narrowing in TypeScript, but that's an article for another day. For now, just know that TypeScript has an eye on your conditional statements and will understand which types are possible within them.

## Any, Unknown, and Never
TypeScript has a few special types: `any`, `unknown`, and `never`. Let's start by taking a look at `any`:

{% gist https://gist.github.com/Max-M-Feige/67ed433620dc74621a70741a08d4d723 %}

Thanks to `any`, the above code is perfectly acceptable and will not yield any errors! A variable of type any has the flexibility to be set to whatever value you want, and other variables of different types can be set to it without issue. In other words, TypeScript does no type-checking on a variable of type `any`. While you might occasionally want a variable to be treated like this, type-checking is one of the best reasons to use TypeScript in the first place. This means that it's usually best to use `unknown` instead.

{% gist https://gist.github.com/Max-M-Feige/824849f68a969e41b4089365b09125fa %}

Although you can set an unknown variable to whatever value you want, other variables cannot be directly set to it. Instead you must explicitly cast its type, like so:

{% gist https://gist.github.com/Max-M-Feige/d44977a2fa64b0bef8218f6e503b979b %}

Using `unknown` is preferred to using any because it encourages us to explicitly pick when and how we convert it to another type.
Finally, let's look at `never`:

{% gist https://gist.github.com/Max-M-Feige/760ef6679471b61b3a935b9f6384dbdd %}

The `never` type is restrictive in that it prevents the assignment of any values to it. Similarly, no other variables can be set to a variable of type `never`. This type symbolizes an impossibility; something that should _never_ happen. One common place we may see never is in functions that should always throw errors and _never_ return a value:

{% gist https://gist.github.com/Max-M-Feige/5ff48395cbdb3d7687e3bf693c42799b %}

I've made a short summary chart for these three types:

![Recommended usage of any,unknown,never chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vnqzimvudd9dpbpft82x.png)

All three of these serve distinct purposes, each with different balances between type safety and flexibility. While `any` provides maximum flexibility, it comes at the cost of losing the type safety benefits given by TypeScript. Because of this, you should instead use the `unknown` type and explicitly tell TypeScript to cast your variables. Lastly, the `never` type will always give us an error when we try to interact with it. We use `never` when we want to tell TypeScript that there should be no scenario in which an outcome occurs, and that you want an error to be thrown if it does. We'll go deeper into some advanced uses of never in future articles.

## Literal Types

TypeScript also gives us literals, another powerful feature that represents exact values and can be a specific instance of a `string`, `number`, or `boolean`. Let's look at one:

{% gist https://gist.github.com/Max-M-Feige/e0aa1956889536cc916537e4f1374d30 %}

Here we created a variable called userRole with the type "basic". Because of this, the variable myUserRole can no longer be assigned to any other string. A literal on its own doesn't seem that useful, but when combined with some of the other functionalities we've explored we can construct some useful types:

{% gist https://gist.github.com/Max-M-Feige/515ce80bc0954ddf4f57bae7d127ba15 %}

Through a combination of unions and literals, we've created a type that could have one of three possible values. This could come in handy!

When we create a variable with `let` or `var`, TypeScript typically assigns it a general type (like `string`, `number`, or `boolean`). Alternatively, if we declare a variable with `const`, TypeScript will assume that its type is a literal. Let's check out an example of this:

{% gist https://gist.github.com/Max-M-Feige/461a48e80004303548cec4068b428180 %}

We can also tell Typescript to behave this way when declaring with let or var by using as const.

{% gist https://gist.github.com/Max-M-Feige/8be0a2eb90fc4f3acecfa5ff699d644e %}

`as const` is a helpful way to make sure that your types are as specific as possible. Additionally, we can use `as const` on objects to ensure that the fields are specific:

{% gist https://gist.github.com/Max-M-Feige/cb7a5c65621eca5257b4d5e1fc8971a6 %}

Overall, literals are an excellent way to limit the values our variables can contain. You can see that `as const` has changed the type of our object's fields into literals. It's also added the keyword `readonly`, though. What's happening here?

## Readonly


![No writing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbx4pr5ldnypn2ccj6qz.png)

TypeScript introduces several new keywords, one of which is `readonly`. This keyword allows us to specify parts of an object as mostly unchangeable. Let's investigate:

{% gist https://gist.github.com/Max-M-Feige/eb546d331a56ae14564c26ac08103717 %}

Now TypeScript will give us an error whenever we try to change (or "write to") the id variable, hence the name `readonly`. The only time a `readonly` property can be set is during initialization, or when the object is created. This concept also applies to classes:

{% gist https://gist.github.com/Max-M-Feige/86d2371b9e4264ed35376034e08bca5f %}

Just like with our interface example, we can't change the id field after it's been initialized. For a class, this means we can only change id in the constructor. After the constructor has finished executing, TypeScript will no longer allow us to change that field.

Finally, readonly can be applied to arrays:

{% gist https://gist.github.com/Max-M-Feige/425d54e67e499968dbe54ea4456b04c7 %}

This prevents us from adding, removing, or changing any element in our array, but we can still assign a new array to the variable containing it.

`readonly` is an excellent keyword to use in large codebases, as it allows you to explicitly choose when something should be observable but unchangeable. This bars people from accidentally modifying data they shouldn't be able to, which in turn prevents bugs and clarifies the intent of your code.

## Conclusion
The features explained in this article are among the most frequently used in TypeScript, and by thoughtfully utilizing them yourself you can construct less error-prone systems and maintain JavaScript's inherent flexibility. Remember, TypeScript isn't about making your life harder! It's about making your code safer, more maintainable, and easier to understand.
As you continue to use and explore TypeScript, I encourage you to try out these concepts and discover for yourself how you can apply them to your own projects. In the next article, we'll dive deeper into some of the common utility types given by TypeScript. Stay tuned!