Immutable TypeScript

const assertions are surprisingly similar to const expressions in C#/C++

I'm mainly a C# programmer at the moment, and when records were introduced in C# 9 (and then record structs and record classes, later in C# 10) I was ecstatic. Finally, a simple way to declare immutable objects!

But now I am learning TypeScript since I want to expand my skills a bit and try some Node.js and React projects, and I was thinking that TypeScript doesn't have such a cool feature and it made me a bit sad.

Turns out, it has something else that is similar! It's called const assertions!

First: what are type assertions?

TypeScript has this syntax where you can tell the compiler that an object is of a specific type after declaring it. It's called a type assertion.

Type assertions are useful, for example when you have a union type returned from a function and the compiler does not yet know the concrete variant of that type, but you do, and you know you don't need to check each variant by writing a complex or long switch statement on a discriminator (if it's a discriminated/tagged union), so you want to cast to the correct type.

Well, type assertion is type casting in TypeScript!

For example, if you use the http module from Node.js, and you call listen() on an http.Server instance and give it a port and a hostname, then calling server.address() will return a string | AddressInfo | null union type, even though the returned address will, in practice, always be an AddressInfo type and never a string, and rarely a null.

It's only null if you call address() before the server finishes binding to the given IP socket, and a string is only returned if you called listen() with a pipe name or a Unix domain socket.

In this situation you can assert to TypeScript that the return type is an AddressInfo type, like this:

Asserting that the return type is an AddressInfo type

If you try to type your object before assigning to it, you will get an error:

Trying to type the declaration as an AddressInfo type results in an error

Type 'string | AddressInfo | null' is not assignable to type 'AddressInfo'. Type 'null' is not assignable to type 'AddressInfo'. ts(2322)

So what do C# records have to do with TypeScript const assertions?

Well, they provide some of the same functionality. It's mainly the readonly aspect of records. Automatic value-type-like equality is unfortunately not given to you for free by any current TypeScript feature. πŸ˜” You still have to implement a custom equals method.

Const assertions are similar to const expressions (C++, C#): they are values that can be evaluated at compile type, and in TypeScript they become like literal values effectively.

From the release notes:

TypeScript 3.4 introduces a new construct for literal values called const assertions. Its syntax is a type assertion with const in place of the type name (e.g. 123 as const). When we construct new literal expressions with const assertions, we can signal to the language that:

  • no literal types in that expression should be widened (e.g. no going from β€œhello” to string)
  • object literals get readonly properties
  • array literals become readonly tuples

This is different from const declarations, which mean that a variable cannot be reassigned after its declaration, but it does not prevent you from modifying any properties of the object that that constant refers to, and it does not prevent you from modifying any elements of an array.

Type widening is another problem, so to speak, because in TypeScript const x = "someString" gives x the type "someString", which is a literal type, while let x = "someString" gives it the type string, which is a much wider type.

Similarly, object literals are free to be modified (i.e. const objLiteral = { foo: "bar" }). Asserting to TypeScript that they are consts will effectively make all their properties readonly. You can check this yourself by hovering over a const asserted object literal in your favorite TypeScript IDE. You should see readonly automatically added to each property declaration.

Also, arrays, like the docs say, will be turned into readonly tuples (let roTupleArray: readonly [number, number, string] = [1, 2, "three"]).

Time for some examples

Finally, here are some practical examples:

let literalVar = 123 as const;
// this is just like saying const literalVar = 123,
// but the errors of reassigning are different
// because 123 becomes a literal type.
// Thus the error of then saying
//   literalVar = 333;
// would be:
//   Type '333' is not assignable to type '123'.
// not:
//   Cannot assign to 'literalVar' because it is a constant.
// as would be the case with a const declaration.

const doSomething = (n: number) => {
  return {
    type: "SomeType",
    property1: n
  } as const;
}

// alternatively you can use the angle brackets
// type assertion, if you're not using JSX:
const doSomething = (n: number) => {
  return <const>{
    type: "SomeType",
    property1: n
  };
}

// both functions above will return an object of type
//   { readonly type: "SomeType"; readonly property1: number }
// this can lead to better type safety, especially in
// stringly-based APIs, like with Redux (ugh!), as you
// can no longer pass invalid string literals!

const a: readonly number[] = [1,2,3];
a.push(4);
// Property 'push' does not exist on type 'readonly number[]'.

const a: readonly [string, number] = ["string", 1];
a.push(2);
// Property 'push' does not exist on type 'readonly [string, number]'.

const a = ["blah",2,3] as const;
a.push(4);
// Property 'push' does not exist on type 'readonly ["blah", 2, 3]'.

// the first two arrays above are explicitly declared readonly tuples,
// but with the third array you can see that with const assertions,
// the resulting type is like a literal type, which is a as 'constant'
// as you can be. 😁

Hope you liked this and found it useful.


Just a short note about myself and how I'm learning TypeScript

The lack of strong typing is what kept me from learning JavaScript when everyone seemed to recommend it as the no. 1 programming language in the world. I thought everyone recommending it was probably mad or just didn't care about type safety and probably liked building big balls of mud.

This led me to have this preconceived notion that TypeScript was probably just as "bad" and just a soon-to-fail effort to try and fix what is a "broken" language.

It's been a long time since I thought this, with many lines of code written in multiple languages, and while I still think JavaScript is probably not the best programming language in the world or the best for me, I have learned that developer productivity is not a thing to be taken lightly!

JavaScript just makes developers more productive, especially in the beginning, and especially for frontend, which is where it originated.

But nowadays it is used in the backend as well, and sooner or later, if you work on a project long enough, you hit a limit where that increased initial speed starts to bite you in the arse. It's no wonder that TypeScript is gaining rapidly in popularity over JavaScript and many other mainstream languages too!

TypeScript gives you the best of both worlds, the dynamic and the static, and allows you to adopt type safety incrementally. Although that's never been my option. I prefer to strongly type everything from the start. It just comes natural to me and always saves me from headaches.

Β