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()
Will become
const x = getText()
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
}
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
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>
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.
Top comments (2)
Thx for sharing, there is one thing that caught my attention:
I wonder: Are you aware of the
unknown
type? It basically forces you to check and therefor is a good alternative toany
(in some cases).Yes, I use it in places where I'm too dumb to type correctly, haha.