DEV Community

loading...
Fullstack Frontend

A Basic TypeScript Insight for JavaScript Devs

K
・4 min read

Since I started working on my SaaS product, I've learned and worked with TypeScript for some months now. My frontend, backend, and even infrastructure code is written in TypeScript, and I quite enjoy sharing interfaces between these parts of my application with a mono repo.

TypeScript is an excellent addition to JavaScript, but some things took me some time to get into my head, one of them being union types.

This all might seem obvious for people used to static typing, but for me, it wasn't evident at first :D

Type Annotations

One of the essential features of TypeScript is annotating your variables and functions with types that are based on JavaScript types but will be entirely invisible for JavaScript later.

Tools like ESBuild will throw away all TypeScript specific syntax and bundle up the remaining JavaScript.

const x: string = getText()
Enter fullscreen mode Exit fullscreen mode

Will become

const x = getText()
Enter fullscreen mode Exit fullscreen mode

Now, this is all nice and good, but it gets confusing with all the types that don't have a direct equivalent in JavaScript.

The any Type

The any type is classic; it tells TypeScript to close both eyes and let you do what you want. If you understand JavaScript, it can sometimes be easier to write one line with any than ten lines correctly typed with TypeScript.

Often it's nice to start with any to get the type checker to shut up, then program the code as you would with JavaScript and later sprinkle actual types on it.

In this example, I access the someKey field without checking anything first. It could be that x is undefined or an object or whatever; I don't care and tell TypeScript that I don't care.

function f(x: any) {
  return x.someKey
}
Enter fullscreen mode Exit fullscreen mode

It's an untyped type that doesn't have any equivalent in JavaScript other than it could be ... well, any type, haha.

This brings us to one of the hard things for me to understand with static typing in general. Later it will be more obvious, but I think it's already the case with any.

There are types in TypeScript that map to multiple JavaScript types at runtime, either implicitly with any, or explicitly with unions.

It didn't bother me with any because it's a particular case of all types, but later it threw me off with union types.

Union Types

Union types are multiple types at once at runtime, like any; the difference is, union types aren't all but only pre-defined specific types.

type StringOrNumber = string | number
Enter fullscreen mode Exit fullscreen mode

The StringOrNumber type only allows using a variable typed with it only in contexts where a string and a number can be used. Otherwise, you must manually check that it's one of both before using it in a string or number context.

While the name and types I have chosen in this example make this obvious, this isn't often the case in an actual codebase.

The type can have any name, and the union can include any type, even generics.

As a JavaScript developer, I was used to the fact that the type was either unknown and I had to check it (the any case) or know what was going on, and I probably was working with some class that wraps some functionality.

This made using unions supplied by frameworks or libraries not easy to understand for me. Sure, one day, I looked at their definition and was baffled by how simple they were, but I was first confused.

But union types are neither. They tell you before runtime that you can use multiple types in one case, but the union type itself doesn't exist at runtime at all. There is no class called StringOrNumber; there is string or number.

If you then couple this feature with another syntax like modules and generics and use a name that isn't as obvious as StringOrNumber, things get even more confusing for the mere JavaScript pleb.

type Result<T> = T | Promise<T>
Enter fullscreen mode Exit fullscreen mode

First, I'm baffled was T is; I mean, sure, it makes Result generic, but why doesn't it get a speaking name? Then Result isn't any more speaking than T either. But what are you going to do? Types as general as this one do need general names.

A variable annotated with Result<string> can either contain a string or a Promise<string>, a promise that resolves to a string.

There is never a Result; it doesn't exist at runtime even if the name Result looks like it (more so than StringOrNumber). It's not something like a class that wraps a value or a promise for that value; it's gone at runtime.

If you wanted to check this in JavaScript explicitly, you would have to either know what you're doing and decide how a T is different from a Promise<T> or wrap it somehow, but this isn't needed in TypeScript. It forces you to think before you write, so you don't have to implement abstractions that have runtime costs.

Sure, you have to check what it is before using it, but you don't have to learn any new class methods or something to use it.

Conclusion

Look at type definitions, don't get fooled by some name that sounds cryptic, too general, or just like a class you might have implemented in the past.

And always keep in mind that (at least most of) TypeScript is just JavaScript, and it goes entirely away at runtime.

A type that doesn't exist at runtime doesn't require you to learn more than you already know about JavaScript.

Discussion (2)

Collapse
karfau profile image
Christian Bewernitz

Thx for sharing, there is one thing that caught my attention:

[...] I was used to the fact that the type was either unknown and I had to check it (the any case) or know what was going on [...]

I wonder: Are you aware of the unknown type? It basically forces you to check and therefor is a good alternative to any (in some cases).

Collapse
kayis profile image
K Author

Yes, I use it in places where I'm too dumb to type correctly, haha.