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 :)
Top comments (40)
It's worth noting that transpilation and compilation are not exactly the same thing. Transpilation is a style of compilation that translates code from one language into another. In typescript that's theoretically really simple because it is a superset of JavaScript. Presumably if you remove all type information from the typescript file, it could be run like a normal JavaScript file. This is not exactly the case, but you get the difference with a language like C that gets compiled into machine code. Transpilation is typically really fast, while compilation can take a little longer. There are actually several runtimes now (bun/deno) that can run typescript code directly, without any transpilation.
Oh nice. Thanks for the info. I wasn’t aware that bun and deno could do that!
Small correction about bun/deno. Both indeed execute TS without transpilation when do it directly, BUT if you launch your app in the browser they will use esbuild to transpile code into JS.
That's because Deno and Bun are Runtime Environments for Javascript/Typescript.
If you want to run your app in Browser (which contains a different Runtime Environment) you need to give it what it supports (JS in this case).
I don't know what's the sense on using Deno or Bun if your target is the browser in the first place but here's the explanation.
To be clear, from Bun's documentation: "Bun natively supports TypeScript out of the box. All files are transpiled on the fly by Bun's fast native transpiler before being executed. Similar to other build tools, Bun does not perform typechecking; it simply removes type annotations from the file."
Oh well, that's more lazy even, thank you for letting me know! <3
the article is nice but the
satisfies
looks like a non-sense to me ... in JS:A constant that points at another constant is the other constant itself. If
b
is exactlya
it makes no sense thatb satisfies Person
buta does not satisfy Person
, they are the exact same reference to the exact same pointer in memory.I think this was a genuine mistake form TS itself and I get people trying hard to explain the difference but if JS is what matters at the end: there is no difference.
Specially with composition, I find extremely hard to also justify this:
It looks like a limbo between casting where
satisfies
act like an enabler at distance but can't validate the "satisfaction" of the literal source of an object.Once again, it's not about your post, rather about the fact you are giving for granted that
satisfies
behavior makes sense only because the master of TS or TS itself told you so ... I think they could've done a better job with a single keyword that is ambiguous for no reasons and it honestly makes zero sense at the JS level.I agree, honestly the more I learn about TS the less it seems necessary. Just kind of seems like it's forcing Javascript to do something it wasn't designed to do.
that's where I'd draw a line between being a superset VS a shenanigans based logic ... luckily enough, I still don't need TS in my projects as they are all core JS enablers, no need for this mental muscles unnecessary overhead 🤷♂️
TypeScript isn't really inferring anything here. You told TypeScript that the type of the properties should be either a string or a number :)
Yes, you're right. What I should have said is that typescript did not infer the narrower type I defined. :)
Informative.
I also like your honest style of writing.
Thanks, Daniel.
Thanks for sharing this! Admire your style of writing mate.
TLDR- I’ve been writing every language without understanding it. 😂😂😂
Typescript is a linter
Just in the sense of static code analysis but the similarities end there. Sadly though 😅 If that were the case we wouldn't require ESLint and additional sh*tload of dependencies which would be nice
With the Bun runtime, you can run TypeScript as is. No transpiling needed. Also runs CommonJS and ESmodules without special file name extensions.
Transpilation by bun or deno is happing when you launch your app in the browser.. because non of modern browsers know something about TS yet (i know that community is working on it)
This is really good. I enjoyed the reading.
thanks!
Superb article
Some comments may only be visible to logged-in visitors. Sign in to view all comments.