I admit, I don’t really understand TypeScript
The other day, I was stuck with a bug in some code that was handling optimistic updates, so I asked my colleague Filip for some help. Filip, a TypeScript wizard, mentioned that the satisfies
keyword would be part of the solution I was looking for.
Satisfies
? What the heck is that? And why had I never heard of it before? I mean, I’ve been using TypeScript for some time now, so I was surprised I didn’t know it myself.
Not too long after that, I stumbled across this tweet from @yacineMTB, prolific yapper and engineer at X.com (aka Twitter):
like, why can't i just run a typescript file? what's the point of a scripting language if i need to init a whole directory and project along with it?
Again, I found myself wondering why I didn’t already know that about TypeScript. Why couldn’t you actually run a TypeScript file? What was the difference between a scripting language and a compiled language?
It hit me that I didn’t quite understand some fundamental things about the language I was using nearly every day to create things like Open SaaS, a free, open-source SaaS starter.
So I decided to take a step back, and did some investigating into these topics. And in this article, I’m going to share with you some of the most important things I learned.
What Type of Script is TypeScript?
You’ve probably already heard that TypeScript is a “superset” of JavaScript. This means that it’s an added layer on top of JavaScript, in this case, that lets you add static typing to JavaScript.
it’s kind of like TypeScript is the Premium version of JavaScript. Or, put another way, if JavaScript were a base model Tesla Model 3, TypeScript Would be the Model X Plaid. Vroooom.
But because it is a superset of JavaScript, it doesn’t really run the way JavaScript itself does. For example, JavaScript is a scripting language, which means the code gets interpreted line-by-line during execution. It was designed this way to be run in web browsers across different operating systems and hardware configurations. This differs from lower-level languages like C, which need to get compiled into machine code first for specific systems before it can be executed.
So, JavaScript doesn’t have to be compiled first but gets interpreted by the JavaScript engine. TypeScript, on the other hand, has to get converted (or ”transcompiled”) into JavaScript before it can be executed by a JavaScript engine in the browser (or as a standalone NodeJS app).
So the process looks a bit like this:
→ Write TypeScript Code
→ “Transcompile” to JavaScript
→ Interpret JavaScript & Check for Errors
→ JavaScript Engine Compiles and Executes the Code
Pretty interesting, right?
But now that we’ve got some of the theoretical stuff out of the way, let’s move on to some more practical things, like the thing TypeScript is known for: it’s Types!
By the way…
We're working hard at Wasp to create the best open-source React/NodeJS framework that allows you to move fast!
That's why we've got ready-to-use full-stack app templates, like a ToDo App with TypeScript. All you have to do is install Wasp:
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
and run:
wasp new -t todo-ts
You'll get a full-stack ToDo app with Auth and end-to-end TypeSafety, out of the box, to help you learn TypeScript, or just get started building something quickly and safely :)
Playing Around with satisfies
Remember how I asked my colleague for help, and his solution involved the satisfies
keyword? Well, to understand it better I decided to open an editor and play around with some basic examples, and this is what I found the be the most useful thing I learned.
To start, let’s take the example of a person object, and let’s type it as a Record
that can take a set of PossibleKeys
and a string
or number
as the values. That would like look this:
type PossibleKeys = "id" | "name" | "email" | "age";
const person: Record<PossibleKeys, string | number> = { }
The way we typed the person
constant is called a Type Annotation. It comes directly after the variable name.
Let’s start adding keys and values to this person
object:
type PossibleKeys = "id" | "name" | "email" | "age";
const person: Record<PossibleKeys, string | number> = {
id: 12,
name: "Vinny",
email: "vince@wasp-lang.dev",
age: 37,
}
Looks pretty straightforward, right?
Now, Let’s see how TypeScript inferred the types of the person
properties:
Interesting. When we hover over email
, we see that TypeScript is telling us that email is a union type of either a string
OR a number
, even though we definitely only defined it as a string
.
This will have some unintended consequences if we try to use some string
methods on this type. Let’s try the split
method, for example:
We’re getting an error that this method doesn’t work on type number
. Which is correct. But this is annoying because we know that email
is a string.
Let’s fix this with satisfies
by moving the type down to the end of the constant definition:
type PossibleKeys = "id" | "name" | "email" | "age";
const person = {
id: 12,
name: "Vinny",
email: "vince@wasp-lang.dev",
age: 37,
} satisfies Record<PossibleKeys, string | number>;
Now, when hover over the email
property, we will see it is correctly inferred as a string
:
Nice! Now we won’t have any issues using split
to turn the email
into an array of strings.
And this is where satisfies
really shines. It let's us validate that the Type of an expression matches a certain Type, while inferring the narrowest possible Types for us.
Excess Property Checking
But something else strange I noticed when I was playing with satisfies
was that it behaved differently if I used it directly on a variable versus on an intermediate variable, like this:
// Directly on object literal
const person = { } satisfies PersonType;
// Using on intermediate variable
const personIntermediate = person satisfies PersonType
Specifically, if I add another property to the person
object that doesn’t exist in the type, like isAdmin
, we will get an error when with the direct use, but we won’t with the intermediate variable:
- Directly using
satisfies
- Using
satisfies
with an intermediate variable
You can see that in example 2, there is no error and person “satisfies” the PersonType
, although in example 1 it does not.
Why is that?
Well, this actually has more to do with how JavaScript fundamentally works, and less to do with the satisfies
keyword. Let’s take a look.
The process occurring in the examples above is what’s referred to as “Excess Property Checking”.
Excess property checking is actually the exception to the rule. TypeScript uses what’s called a “Structural Type System”. This is just a fancy way to say that if a value has all the expected properties, it will be used.
So using the personIntermediate
example above, TypeScript didn’t complain that person
had an extra property, isAdmin
, that didn’t exist in the PersonType
. It had all the other necessary properties, like id
, name
, email
, and age
, so TypeScript accepts it in this intermediate form.
But when we declare a type directly on a variable, as we did in example 1, we get the TypeScript error: “’isAdmin’ does not exist in type ‘PersonType’”. This is Excess Property Checking at work and it’s there to help you from making silly errors.
It’s good to keep this in mind, as this will help you to avoid unintended side-effects.
For example, let’s say we change the person type to have an optional isAdmin
propert, like this:
type PersonType = {
id: number,
name: string,
isAdmin?: boolean, // 👈 Optional
}
What would happen if we accidentally defined person
with an isadmin
property instead of isAdmin
and didn’t declare the type directly?
We would get no error from TypeScript because person
actually does satisfy all the necessary types. The isAdmin
type is optional, and it doesn’t exist on person
, but that doesn’t matter. And you’ve made a simple type-o and now are trying to access the isAdmin
property and it doesn’t work:
Whoops! Let’s fix it with a type annotation, where we declare the type right away:
Nice. Because we used a direct type annotation on line 58, we get the benefits of TypeScript’s excess property checking.
Thanks, TypeScript! 🙏
If you found this content useful, and want to see more like it, you can help us out really easily by giving Wasp a star on GitHub!.
To Be Continued…
Thanks for joining me on part 1 of my journey into better understanding the tools we use everyday.
This will be an ongoing series where I will continue to share what I learn in a more exploratory, and less structured, way. I hope you found some part of it useful or interesting.
Let me know what you’d like to see next! Did you enjoy this style? Would you change something about it? Add or remove something? Or do you have an opinion or similar story about something you’ve learned recently?
If so, let us know in the comments, and see you next time :)