Eccentric Developments


Types Are Your Friends

I usually write about programming languages and 3D rendering; however, this post, while programming language related, is more about me trying to solidify and express my thoughts on a concept of programming languages: Type Systems.

Types are a component of some programming languages that help you, as the developer, better express your intentions on what you are doing, and in doing so, help others (including your future self) have a clearer picture on what some code is doing. This delivers a better understanding of the problem, and helps prevent bugs.

The use of types is something that I didn't truly grasp when I was learning how to develop applications, in fact, I felt that types were an obstacle that just got in the way of implementation speed, as such, I was quite a fan of JavaScript. Later, I understood that types are one of the best tools you can leverage to ensure that a software system is correct.

This is why I care about this topic: good use of types helps avoid subtle bugs in applications, enhances tools like IntelliSense, and makes for much easier to maintain software.

Types Are Simple

Types are a very simple concept to understand in programming languages, they are mainly annotations that put constraints on the data that can be stored in a variable. Those constraints are usually primitives like integers, float numbers, strings, and compound structures that the developer can create.

The constraints created by types are checked either statically or dynamically, and help determine if a value can be assigned to a variable or bound to a function argument; they also help inform the reader of what operations can be executed on top of it. For example, when a piece of data is defined as a string, the compiler (statically) or the runtime (dynamically), will prevent a numeric division operation from happening to it.

Of the two types of type check systems, the one I think is the most useful is static checking, because this happens while the application is being compiled, surfacing errors early. This will effectively prevent the final application from having a data compatibility bug during runtime.

There are several different programming languages that offer static typing, and they all offer varying degrees of strictness; programming languages like Haskell, Rust, OCaml, TypeScript and many others have great type systems that are statically checked.

For the purpose of this article, I'll focus on TypeScript.

TypeScript Has Types

In TypeScript, defining the type of a variable looks like this:

let userId: string = "some-id";

This declaration of the userId variable defines its type as a string. Meaning that, for every operation attempted on it, the compiler will check that it is applicable to the string type. For instance, trying to change the value to a number: userId = 10.0 will cause a compiler error.

These kinds of checks not only let us know that we are trying to assign an incompatible value to userId, they also prevent invalid state, the associated bugs with it, and give context on what is happening in the code.

The compiler is making sure that our assumptions about the variable userId (i.e. that it is a string) are valid for the rest of the scope of the variable. As such, it makes it safe for the code to execute string operations on it, and gives the developer context on the current state of the program.

Types also help when declaring the arguments of a function, for example:

function getUserDetails(userId: string): UserDetails {
    // do stuff
}

Similar to the previously mentioned variable, in this function argument, the assumptions that can be made about userId are simple, but very useful:

  1. It contains a string value
  2. It is not null or undefined

This means that for the getUserDetails operation, we do not need to be as thorough with the usual defensive programming, in other words, there is no need to check for null or undefined at the beginning of the getUserDetails function. The compiler will check that the value of userId is a valid string during compile time.

So far, the string check is very useful, but it will not prevent some more subtle errors. For instance, the compiler will not prevent the following from happening:

const sessionId: string = getCurrentSessionId();
const userId: string = getCurrentUserId();
const userDetails: UserDetails = getUserDetails(sessionId); // This should have been userId

Basic types can get you only so far, a string type, tells you that the variable will contain a valid string, but it is incapable of telling you if its value is what you expect.

Nonetheless, some type systems let you take matters into your own hands and enable you to create custom types that contain more information about the data they represent. As such, if we want to prevent the previous bug from being representable, we can create a new type for userId:

type UserId = {
    _type: "UserId";
    id: string;
};

type SessionId = {
    _type: "SessionId";
    id: string;
};

The UserId and SessionId types are more than simple strings now; they are distinct types that, while they have a similar internal structure, are treated as different by the compiler. This distinction makes it so the following code will produce a compile-time error:

function getSessionId(): SessionId {
    return {
        _type: "SessionId",
        id: "sessionId"
    };
}

function getUserDetails(userId: UserId) {
}

const sessionId: SessionId = getSessionId();

getUserDetails(sessionId);

source

Trying to bind a SessionId to a UserId parameter will output:

Argument of type 'SessionId' is not assignable to parameter of type 'UserId'.

With this new type, UserId, the potential bug caused by binding a sessionId value to the getUserDetails function gets eliminated, alongside multiple future headaches. Unless you do the wrong thing and force compliance using the as keyword or disabling the type checks by using the any type, please don't.

Much More

Effective use of types can make refactors much safer; if part of your task is to rename a member of an object, the compiler will help you identify every other piece of code that needs to be updated. This way, you don't have to rely only on the text editor's find/replace.

Types can make invalid state difficult to represent. In 3D rendering, whenever you want to do an intersection test, depending on the algorithm, it is required to use a normalized vector, which is similar to a regular vector (with the x, y and z coordinates) but with the characteristic of having a unit norm. If such an intersection algorithm is passed a non-normalized vector, the result will be incorrect, and hard to debug; I am speaking from experience here.

To prevent this subtle bug, an updated intersection function could only accept normals instead of vectors, and the application can only allow the creation of normals as the result of normalizing a vector. This way, we can be sure that all executions of the intersection function will receive the correct data.

Conclusion

Types can be annoying when you try to move fast, but when used the right way, the friction they present is paid many times over with more resilient software and easier to understand code.

But I need to stress that types need to be used the right way, using escape hatches like TypeScript's any, is basically going back to vanilla JavaScript, and preventing all forms of type checking and disregarding any meaningful context.

Like everything else, effective use of types takes effort and practice; not everything needs a custom type while some very simple things do, as illustrated in the previous sections. Nonetheless, types are there to help you—so use them!


Enrique CR - 2025-07-30