DEV Community

Cover image for I've been writing TypeScript without understanding it
vincanger for Wasp

Posted on

I've been writing TypeScript without understanding it

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.

Image description

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?

Image description

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.

Image description

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and run:

wasp new -t todo-ts
Enter fullscreen mode Exit fullscreen mode

Image description

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> = { } 
Enter fullscreen mode Exit fullscreen mode

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,
} 
Enter fullscreen mode Exit fullscreen mode

Looks pretty straightforward, right?

Now, Let’s see how TypeScript inferred the types of the person properties:

Image description

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:

Image description

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>;
Enter fullscreen mode Exit fullscreen mode

Now, when hover over the email property, we will see it is correctly inferred as a string :

Image description

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Directly using satisfies

Image description

  1. Using satisfies with an intermediate variable

Image description

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
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

Whoops! Let’s fix it with a type annotation, where we declare the type right away:

Image description

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!.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgbmn45pia04bxt6zf83.gif

⭐️ Star Wasp 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 (34)

Collapse
 
brense profile image
Rense Bakker

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.

Collapse
 
vincanger profile image
vincanger

Oh nice. Thanks for the info. I wasn’t aware that bun and deno could do that!

Collapse
 
eshimischi profile image
eshimischi

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.

Thread Thread
 
joelbonetr profile image
JoelBonetR 🥇

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.

Collapse
 
webreflection profile image
Andrea Giammarchi • Edited

the article is nice but the satisfies looks like a non-sense to me ... in JS:

const a = { name: 'Bob', age: 12 };
const b = a;
b === a;
Object.is(b, a);
Enter fullscreen mode Exit fullscreen mode

A constant that points at another constant is the other constant itself. If b is exactly a it makes no sense that b satisfies Person but a 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:

// what does this do?
const a = {
  ...foreign,
  otherThing: 123
} satisfies Person;

// why is this fine?
const b = a satisfies Person;
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
daniel_nwokocha profile image
Daniel Nwokocha

Informative.
I also like your honest style of writing.

Collapse
 
vincanger profile image
vincanger

Thanks, Daniel.

Collapse
 
michaelmior profile image
Michael Mior

Now, Let’s see how TypeScript inferred the types of the person properties:

TypeScript isn't really inferring anything here. You told TypeScript that the type of the properties should be either a string or a number :)

Collapse
 
vincanger profile image
vincanger

Yes, you're right. What I should have said is that typescript did not infer the narrower type I defined. :)

Collapse
 
devvictory profile image
Karan Sethi

Thanks for sharing this! Admire your style of writing mate.

Collapse
 
justaguyfrombr profile image
Misael Braga de Bitencourt

Typescript is a linter

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

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

Collapse
 
robert_schaffrath_8fa02f5 profile image
Robert Schaffrath

With the Bun runtime, you can run TypeScript as is. No transpiling needed. Also runs CommonJS and ESmodules without special file name extensions.

Collapse
 
eshimischi profile image
eshimischi

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)

Collapse
 
itsarun01 profile image
Arun kumar

Superb article

Collapse
 
ayush2390 profile image
Ayush Thakur

Quite an informative article

Collapse
 
vincanger profile image
vincanger

Thanks, glad you like it :)

Collapse
 
alxwnth profile image
Alex

Nice post, thanks!

Collapse
 
masteing_the_code profile image
Joel Jose

This is really good. I enjoyed the reading.

Collapse
 
vincanger profile image
vincanger

thanks!

Collapse
 
mikhaelesa profile image
Mikhael Esa

This is a great one! Will definitely try the satisfies keyword. Thank you!

Collapse
 
litlyx profile image
Antonio | CEO at Litlyx.com

Great content! It brings real insight on real life!

Thank you!
Keep the great work up!

Antonio, CEO & Founder at Litlyx

Collapse
 
syedmuhammadaliraza profile image
Syed Muhammad Ali Raza

Good one with real life example

Collapse
 
devpugg profile image
Devpugg

Couple of things:

Transpilation is a made up new term, it's just compilation.
Compiling means parsing source code, building an AST and then generating some other source code. Usually, that's lower level code like Assembly, which can be used to produce machine code, but that's an implementation detail.
Some folks thought compiling higher level langs into other high level langs warranted a new term. Personally, I think it's just confusing, but whatever. Just wanted to point out there's nothing special about it.

The distinction between scripting and compiled langs is also a bit misleading. That X user, @yacineMTB, and this article seem to conflate scripting with interpreted, but that's not quite the same thing. It's even more confusing that he then asks for a binary, which is the result of compilation. So he's essentially asking about running a compiled language without compiling it and yet, somehow, magically, still have a binary. Makes no sense.
If you want to run a script, you typically use an interpreter, and you can do that for TS too, via Deno and Bun.

Which brings me to VMs. They don't generate machine code per se, in fact, the whole point is to avoid it. Again, that's a compiler's job. VMs instead take an AST and evaluate it, thereby skipping the code generation step.

Finally, type systems! TS's excess property checking is a semantic rule and not necessarily related to the Structural nature of the type system. It's more about subtyping, which granted, is more frequent in structural systems. A structural type system can still throw a type error in subtyping situations if the definition of type equality demands it.
For example, you could rule {foo: string} != { foo: string, bar: integer }. TS developers decided against it, and while that's a somewhat controversial decision, I personally think it was a good one given the nature of most JS codebases.
If you're interested, other languages, name Purescript and Elm, are far stricter on this and avoid subtyping, instead opting for something like row polymorphism or extensible records. That has its downsides too, though.

Collapse
 
michaelmior profile image
Michael Mior

Transpilation isn't really a "new term." Transpilers have been around since at least the mid-90s.