Type Transformations Workshop (9 exercises)

The Annotation Used to Infer an Object's Values as Read-only Literals

The solution is to add the as const annotation to the programModeEnumMap object as seen below:

There are a couple useful things that as const does for us.

First, it freezes the object's values and ensures that they are inferred as their literals. It also adds the readonly annotation to the object's keys, which makes the properties immutable.

We can see this information when we hover over programModeEnumMap in VSCode:

Without the the as const annotation, the properties are mutable and in this case are typed as strings.

Since we know that the values are never going to change, we add as const to freeze them and infer them as their literal types.

Using as const with Arrays

We can also use as const with arrays.

For example, TypeScript would consider arr a number array and allow us to change items:

Adding the as const annotation makes it so we could no longer change the values, and instead they would be inferred as their literal types:

Deeply Nested Data with as const

We can also use as const with deeply nested data.

If we add a coolThing object property to our programModeEnumMap object, TypeScript would infer the values as their literals all the way down.

Hovering confirms that coolThing is readonly.

Comparing to Object.freeze

We can get a similar result to as const with Object.freeze, though with a couple important differences to note.

Object.freeze works both on the type and runtime levels.

However, it only works on the first level of an object. This means that values that are nested more deeply won't end up as readonly.

To summarize: as const works with deeply nested objects, but only on the type level.

Object.freeze works at both the runtime and type levels, but only on the first nested item.


[0:00] The solution is this, as const annotation. Now, what this does is it does two things. First of all, it freezes the values here and it makes sure that they are inferred as their literals, which is really useful. The second thing is it adds this, read-only annotation to them as well.
[0:18] Now, what happens there is it means that without this as const, these are types as string. This is proper, because these can be altered. We can say, programMode.GROUP = "somethingElse."

[0:32] That means that you need to type this as string, if you're typescript, because you're thinking, "OK, this is actually mutable here." Turning it with, as const, means that this now turns it into a readonly property. GROUP gets inferred as group, because we know it's never going to change.

[0:51] This also works with arrays too. If I say, const arr = [1, 2, 3] , then this is going to be inferred as number array, because I can go in and change any of these to whatever I want. If I say it, as const, then I can't go in and change them and it means they're inferred as their literal types. Super cool.

[1:13] Now, this it even works further down the tree. I can go, coolThing, and then go further down, cool

: "[1:21] cool". You'll notice that it goes all the way. If I remove a couple of these, so we can see the annotation, then we get, readonly coolThing, readonly cool is cool, which is cool.

[1:33] Now, this is slightly different from another way you could do this is with Object.freeze(). This works on the runtime level and on-type level. This means that you will get a readonly type of all of these, which is pretty useful, but Object.freeze only works on the first level. It doesn't go deeper and deeper and deeper.

[1:53] It means, if you have this, cool object down here, whatever

: "[1:57] 123", then that's not going to...Sorry, let me just do this. That's not going to get inferred deeply then. Object.freeze only works on the first level, where as const works all through it.

[2:12] As const also doesn't add anything to the runtime either. It's only a type annotation, but it does mean that all of these types are inferred as their values and inferred is read-only.