Check out a free preview of the full Intermediate TypeScript, v2 course

The "Classes" Lesson is part of the full, Intermediate TypeScript, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Mike demonstrates declaration merging in TypeScript by creating a new class and explaining how declaration merging works with classes. He shows how the class itself is a value and can be invoked to create instances, and how static methods can be considered as a separate namespace or part of the class value. He also discusses the use of the "as any" type assertion and the difference between TypeScript's read-only attribute and the Object.freeze method for creating read-only objects.

Preview
Close

Transcript from the "Classes" Lesson

[00:00:00]
>> Let's look at a more modern place where we see declaration merging in at play. So let's create a new class, I'm gonna call this Fruit2. And I'm gonna give it a name property, which is optionally present, it's a string. And I'm gonna give it a mass property, which is a number.

[00:00:21]
I'm gonna give it a color, which is a string. And then let's give it a static field called createBanana, yeah, that'll work, thanks, Copilot. So, great, we've got Fruit2, let's apply the tests we just learned to figure out what's on this declaration. So I'm gonna change this to Fruit2.

[00:00:50]
All right, so Fruit2 is a value, a class is a value, maybe not super surprising, classes exist in JavaScript. We can call new Fruit2, we're using it as a value there, so this isn't super shocking. The createBanana function is there. This is almost like the jQuery case, where we have the class where we can invoke it to create an instance of Fruit2.

[00:01:22]
But we also have this thing dangling off of it. So this is almost like a namespace here, the static side of a class, right? And then let's look at this, we've got Fruit2, typeTest: Fruit2 equals empty object as any. Fruit2 is passing the test for a type. And we can see that when we start to pull things off of this type, that's evidence that these properties are present on the type.

[00:01:56]
What we're seeing here is that we have been benefiting from declaration merging whenever we've used classes. It's an interface for an instance of the class stacked on top of a value declaration that represents the class itself. The thing we invoke when we say I want a new Fruit2, it's passing both the value test and the type test.

[00:02:28]
The tooltips here, let me put Fruit2 down here just so you know nothing's up my sleeve. It's not showing us multiple things here. And that's because this is a super common use case. And it's an optimized way to represent this. It's the class Fruit2, but there is declaration merging at play.

[00:02:52]
There is a type on this thing, there is a value on this thing. Whether you wanna call createBanana, the static methods on a class, whether you wanna think of that as a namespace or not, it's up to you. Actually, it won't break your mental model either way, because, remember, a namespace is kind of like a value.

[00:03:10]
And so we could say, well, it's like a separate thing from the class definition in the constructor, right? Or you could think of it as like, okay, this is one big value, it's not quite like a function, right? It's not quite like the jQuery case, where we literally have a function called $.

[00:03:33]
You could think of it either way, but classes are both types and values. So I submit to you that there is declaration merging happening here. There's more than one thing riding on the single identifier.
>> It looks like it picked up the types of Fruit2. Cuz the actual method createBanana just return this one option, static, that's always that thing.

[00:04:00]
>> Yep, I can do this, though.
>> Why is it not giving me literals?
>> Why is it not giving you literals, can you say more?
>> On 107, you're returning name as banana, color, yellow, mass, 183. Why is it not thinking it's always gonna be those things, and it's picking up that it's this type up above?

[00:04:24]
>> TypeScript is trying to make a reasonably safe assumption here. I think if I did this, let's see if I do this, there's one more thing I could try, but, right, createBanana, there you go. So I can help you with your mental model here. What happens if I do this?

[00:04:55]
What type is TypeScript gonna think this is?
>> An array?
>> An array of numbers. Why isn't it saying it's a tuple of length 3 with these specific things in it? It'd be inconvenient.
>> That's for the array.
>> Well, also, arrays are mutable, right? If I do this, I'm combining a non-reassignable variable, that's const, we can't reassign it, and I have an immutable value type here.

[00:05:29]
I can't change that one to a different number in memory. And I can't point array to a different number, so this is cemented in place. So where you'll see literal types coming through is where you've stated, look, I have this mutable thing, an object. But I want you to regard it as if this is a read-only thing, infer as if it were a const declaration.

[00:05:58]
And when you do that, you're saying, all right, TypeScript, I'm never changing this, make much more specific assumptions about this. And that's why here we can see, look at that, it's read-only name, read-only color, read-only mass. Imagine we were holding this as a class field or some state outside of createBanana.

[00:06:24]
It doesn't know for sure that by the time it returns this, it will be exactly what it is on this line. Does that said help it click?
>> Yeah.
>> LJ in chat asks, how do we check that fruit is a value? Can someone help me write a test that will only pass if fruit is a value?

[00:06:53]
Go ahead.
>> You could try assigning it to a variable.
>> Only works if it's a value. This line right here, if this were just a type, It would object, so if you can assign something to a variable, it's a value, or it's at least a value, make sure you remember that.

[00:07:19]
It could be other things too, but at least there's a value present. Mahmoud asks, why are we using any on line 117? That is a great question, and I actually ran into this problem earlier, and someone in class helped me figure this out. [LAUGH] it happens to work here.

[00:07:40]
I'm gonna change something, I'm gonna make name a required property here. If I didn't use as any here, and I had some required property on Fruit2, I'm gonna get an error here. Now, I would argue, we're still passing the value test here, sorry, we're passing the type test here.

[00:08:01]
Because the nature of this error, it's saying, all right, it's a type, but you're missing the name property. But it's a type, we're still passing that, it's easier to understand if we say as any. And what we're doing here is we're saying, look, I don't want you to worry about the particulars of actual type equivalence here.

[00:08:25]
Just tell me if it is a type at all, and that, we can suppress any errors about the particulars of comparing is this empty object, does it really satisfy the Fruit2 type? We're suppressing those errors, effectively, and we're saying, just tell me if I have the right thing to put in this part of my code.

[00:08:52]
>> And one more question, what happens if we clone an object as const, does it keep the read-only attribute?
>> How are we cloning it? So I'm gonna answer what I think the real question is behind the scenes. The read-only attributes here, they're just TypeScript trying to stop you from editing these values.

[00:09:22]
It will do nothing for you at runtime, it's not actually read-only. It's more like TypeScript saying, I will yell at you if you try to write to this thing. There is a way you could make something read-only, and that would be this. If we do Object.freeze, you can think of this as like, you're not allowed to write anything to this.

[00:09:49]
I'm curious, let's see, all right, now it's called this Readonly, great, it's a different form. Instead of saying as const, which will go to every property of the value, and will say, okay, this is a literal type, we're saying the whole object is read-only. It's not a mutable object with some read-only fields in it, the whole object is read-only, we can't add new things to this one, right?

[00:10:21]
The more important thing to realize is this is actually a read-only object. If you were to run this code in a browser that has no knowledge of TypeScript. And by that time the readonly keyword that we had before, like this kind of thing. This kind of thing is stripped away after you build and you spit out JavaScript.

[00:10:41]
Object.freeze actually makes the object read-only, and at runtime, it actually cannot be changed. Almost like you replaced all of the fields with just getters of the same name, and there's just no way to set properties. If you attempt to write to this object, then it's just intercepted. I believe the property is, like what you would see, you can attempt to write, and I think it just silently does not happen.

[00:11:10]
You can try to write all you want, it just has no effect. Very important thing to understand, what is the thing that TypeScript is trying to keep you honest about, versus what's something that's actually part of your program? And is really preventing things from happening at runtime. This make me think of, also, the access modifier keyword private, for TypeScript private fields, versus actual, hashtag JS private fields.

[00:11:42]
The latter actually being private, and the former is sort of just TypeScript saying, look, you said that you shouldn't access this. And so I'm gonna fail to type check if you try to access this thing. Did you have a question?
>> Yeah, I guess what's more common in practice is, I mean, to use the TypeScript's read-only kind of method, or to do an Object.freeze?

[00:12:06]
>> I'm gonna say they are different tools. Object.freeze is not free, so if you were to do this on a really hot path, it comes with some cost, and you're doing it to the entire object. So if you wanted an object with a few read-only properties on it, you wouldn't wanna do Object.freeze.

[00:12:29]
There are other things you could do to make that actually enforced at runtime, and you would reach for an ES proxy or something like that. Or you'd write a class with getters where there is no setter for a few properties. But it's really about deciding whether something actually needs to be read-only at runtime.

[00:12:52]
Or whether you just wanna be kept honest about abstaining from writing. I mean, we get the right type out of this, it's this Readonly utility type. And if we were to look at that, it's the same thing as the previous type. The Readonly type, we'll look at it later.

[00:13:19]
But I'll explain it to you now, oops. All it's doing is it's iterating over the keys of a thing and it's adding readonly to each field. It's the same as what we saw before. I tend to reach for Object.freeze pretty rarely. Sometimes in library or framework code where you're like, this should never be mutated, I will see Chrome deoptimize my code if this ever mutates, that's when you would do something like this.

[00:13:55]
But the silent failure nature of it is sort of, can be confusing, it's hard to catch bugs that way. And then you're right back to just TypeScript keeping you honest, so.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now