TypeScript - Type erasure and error behaviour?


TypeScript is a popular programming language that offers features such as type checking and type annotations to help developers write more robust and maintainable code. However, when TypeScript code is compiled into JavaScript, the type information is lost in a process called type erasure. This can lead to errors at runtime that are difficult to diagnose and fix.

In this article, we will explore the concept of type erasure in TypeScript and how it can affect error behaviour in our code.

Type Erasure

Type erasure is the process of removing type information from a program during compilation. In TypeScript, this means that the type annotations we add to our code are removed when the code is compiled into JavaScript. This is because JavaScript does not have a type system like TypeScript, so the type information is not needed at runtime.

For example, consider the following TypeScript code −

function add(a: number, b: number): number {
  return a + b;
}

When this code is compiled into JavaScript, the resulting code looks like this −

function add(a, b) {
   return a + b;
}

Notice that the type annotations have been removed, leaving only the function signature. This means that at runtime, there is no way to know the types of a and b, and any errors that result from passing the wrong types of arguments will occur at runtime.

The browsers or run-time environment are built to run JavaScript code and not TypeScript code. So, every time we want TypeScript code to be run, it is first transpiled into JavaScript. This transpilation process involves removing the type annotations and hence all the provided type checking at the runtime.

Error Behavior

When type information is erased, errors that would normally be caught by the TypeScript compiler can occur at runtime instead. This can make it more difficult to diagnose and fix errors in our code.

Example 1

For example, consider the following TypeScript code −

function add(a: number, b: number): number {
   return a + b;
}
console.log(`The result of addition operation is: ${add("1", "2")}`);

When this code is compiled into JavaScript, the type annotations are removed, and the errors are flagged onto the console.

Argument of type 'string' is not assignable to parameter of type 'number'.

The Javascript code of the above code is as below −

function add(a, b) {
   return a + b;
}
console.log(`The result of addition operation is: ${add("1", "2")}`);

Output

The result of addition operation is: 12

At runtime, the console.log statement will output "12", which is not what we expected. This is because we passed two strings instead of two numbers to the add function, and since the type information was erased, the function was able to execute without throwing an error. To fix this, we would need to add runtime checks to ensure that the arguments passed to add are actually numbers.

Unlike other traditional compilers (GCC or Clang), the TypeScript compiler (tsc) doesn’t stop the compilation process once any error (type-related error) is encountered. It goes anyway and finishes the compilation of the whole code. It only just flags and displays the errors on the console but continues with the compilation.

Example 2: Object Type Checking

Another example of error behaviour that can occur due to type erasure is with object properties. Consider the following TypeScript code −

interface Person {
   name: string;
   age: number;
}

function printInfo(person: Person) {
   console.log(`The name is: ${person.name}`);
   console.log(`The age is: ${person.age}`);
   console.log(`The email is: ${person.email}`);
}

const john = { name: "John", age: 30, email: "john@example.com" };
printInfo(john);

At runtime, the printInfo function will still execute without throwing an error, even though the john object has an additional email property that is not part of the Person interface. This can lead to unexpected behaviour if the printInfo function relies on the Person interface to be complete. To fix this, we would need to add runtime checks to ensure that the person argument passed to printName only contains properties that are part of the Person interface.

On compiling, the above TypeScript code will generate the following JavaScript code −

function printInfo(person) {
   console.log("The name is: ".concat(person.name));
   console.log("The age is: ".concat(person.age));
   console.log("The email is: ".concat(person.email));
}
var john = { name: "John", age: 30, email: "john@example.com" };
printInfo(john);

Output

The name is: John
The age is: 30
The email is: john@example.com

The basic philosophy while developing the TypeScript compiler was to flag any errors in the code during the early development phase (including both the type and JavaScript errors) but still compile it into JavaScript code.

Example 3: Generics Type Checking

function identity<T>(value: T): T {
   return value;
}
console.log(`The returned value is: ${identity<number>("1")}`);

In this example, we have a generic function identity that takes a value of type T and returns it unchanged. However, we pass a string instead of a number to the function.

Since the type information is erased, the function is able to execute without throwing an error, but the result is not what we expected. This can be fixed by adding a runtime check to ensure that the value passed to the function is actually of type T.

On compiling, the above TypeScript code will generate the following JavaScript code −

function identity(value) {
   return value;
}
console.log("The returned value is: ".concat(identity("1")));

Output

The returned value is: 1

Conclusion

Type erasure is an important concept to understand when working with TypeScript. It can lead to errors at runtime that are difficult to diagnose and fix, especially when they are caused by passing the wrong types of arguments or by object properties that do not match the expected interface. To mitigate these errors, it is important to add runtime checks to ensure that our code behaves as expected. Additionally, we can use tools such as TypeScript's unknown type and the as keyword to perform type casting and ensure that our code is handling different types of data correctly.

In conclusion, while TypeScript provides robust type checking and type annotations to help us write better code, it is important to understand the limitations of the language and how type erasure can affect error behaviour at runtime. By adding runtime checks and using tools such as unknown and as we can mitigate these errors and write more robust and maintainable code.

Updated on: 01-Aug-2023

120 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements