Skip to main content

Leveraging Type Safety

Leveraging type safety means designing code to maximize the value of your language's type system.

In a type-safe system, the language helps prevent type mistakes before the program runs. For example, it will prevent you from passing a string into a function that expects a number or assigning a number to a variable that expects a boolean value.

In practice, this means many errors are caught before the program runs, instead of appearing later as bugs in production. This is known as compile-time checking.

Type safety helps reduce invalid states inside a system. The goal is simple: make it difficult or impossible for the program to create incorrect data through normal code execution.

Statically vs Dynamically Typed Languages

Programming languages vary in how they enforce type safety.

Statically Typed Languages

Statically typed languages require developers to clearly define the type of data being used.

Examples include:

  • C#
  • Dart
  • Java
  • TypeScript (when used properly)

These languages enforce rules such as:

  • Numbers cannot be treated as strings
  • Functions must receive the expected types
  • Data structures must match defined shapes

Because the compiler understands these rules, it can detect many problems early.

Example:

int age = 25;
// this is not allowed to be compiled, and often shows an IDE error
age = "twenty five";

The program fails to compile because a string cannot be assigned to a numeric value.

This prevents the mistake from ever reaching production.

Compile-Time vs Runtime Errors

A compile-time error happens before the program runs. The compiler detects a problem and refuses to build the program.

A runtime error happens while the program is running. The code compiles successfully, but something goes wrong during execution.

Type systems help move many errors from runtime to compile time, where they are easier to fix and can't corrupt data.

Dynamically Typed or Untyped Languages

Dynamically typed languages allow values to change type freely.

JavaScript is a common example.

let age = 25;
age = "twenty five"; // this is allowed because type is not forced

The program runs, but the variable now contains a different type of value than expected. This can create runtime errors later when the value is used.

Example:

Live Editor
// doesn't the result look odd?
function addOneToString() {
  let age = "25"; // remove the quotation marks and see what happens!
  let nextYear = age + 1;

  return nextYear;
}
Result
Loading...

Instead of performing numeric addition, JavaScript performs string concatenation. In JavaScript, when one operand is a string, the + operator converts the other value to a string and joins them together instead of performing numeric addition. The result is correct according to the language rules, but likely not what was intended.

This is why we prefer TypeScript over plain JavaScript. TypeScript is very similar to JavaScript but adds a type system that catches many mistakes before the program is even run.

let age: number = "25";
// error: Type 'string' is not assignable to type 'number'

// since we specify type, we can avoid that mistake
let ageCorrect: number = 25;
let nextYear: number = ageCorrect + 1;

console.log(nextYear); // result: 26

The Purpose of Type Safety

The primary goal of static typing is to prevent invalid states inside the program.

An invalid state occurs when data exists in a form that the system was never designed to handle.

Ideally, invalid states should only come from outside the system, such as:

  • user input
  • external APIs
  • imported files
  • database data

These external inputs must always be validated.

But once data enters the system and becomes part of our internal models, the program should enforce correct structure and usage.

Type safety helps achieve this by ensuring:

  • variables hold the correct kinds of values
  • functions receive the correct inputs
  • objects follow expected structures

When used properly, type systems prevent entire classes of bugs from ever existing.

Escaping the Type System

Many languages provide ways to bypass type safety.

These features are useful in certain situations, but they should be used carefully.

Examples include:

LanguageEscape Keyword
TypeScriptany
C#dynamic
Dartdynamic

These keywords allow a variable to hold any type of value.

The most common use case for these "escaped types" is handling data from outside of the system where we have no control or previous knowledge of what shape the data is taking. In these situations, it is best practice to first accept data as "untyped" and then use type checking to verify what type it actually is before working with the data.

Example: Error When Using any Type

let value: any = "hello";
value.toFixed();

This is an example of improperly using the any keyword in TypeScript. Since the compiler is unable to know what data to expect in value, it assumes that you know what you are doing and that you can call toFixed() on the value that is stored there.

However, toFixed() is a function that only exists on numbers. It formats a number to a fixed number of decimal places.

When the program runs, the value is actually a string. Since the toFixed() function does not exist on string, the program throws a runtime error.

Once data becomes any or dynamic, the type system can no longer help us. Errors that could have been caught early now appear at runtime.

Whenever possible, these escape mechanisms should be avoided or limited to well-defined boundaries.

Losing Type Safety

TypeScript provides good type safety, but it only works if we actually use it.

Specific to Typescript

This section applies specifically to the TypeScript language. Most statically typed languages do not allow ignoring type definitions.

Example: Untyped Function

Without type definitions, a function can accept any value:

function applyDiscount(price, discount) {
return price - discount;
}

This is valid TypeScript, but is not best practice. In this case, the expected types for the function parameters are not defined. Depending on your TypeScript settings, the system will assume the any type for these parameters and we can pass in any value.

applyDiscount(100, 10);
// result: 90 (works correctly)

applyDiscount("100", 10);
// result: 90 (JavaScript converts the string "100" to a number)

applyDiscount(100, "ten");
// result: NaN (JavaScript cannot convert "ten" to a number)

TypeScript is compiled to JavaScript before it runs. Some of these calls may technically work, but they rely on JavaScript's implicit type conversions. Not defining types can easily lead to bugs, as we can see in this example.

Example: Typed Function

With proper typing:

function applyDiscount(price: number, discount: number): number {
return price - discount;
}

Now incorrect usage is detected immediately.

applyDiscount("100", 10); // compile error

The type system prevents invalid data from entering the function.

Questions to Ask When Using Types

When writing code, it can help to ask a few simple questions:

  • Can this value ever hold the wrong kind of data?
  • Does this function clearly communicate what it expects?
  • Are we relying on implicit type conversions?
  • Are we using any or dynamic where a more specific type would be safer?

If the type system is used well, many of these problems disappear before the program ever runs.