I've been writing TypeScript without understanding it

vincanger - Jun 11 - - Dev Community

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 :)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .