Could TypeScript benefit from built-in runtime type checks?

Published on

TypeScript

TypeScript is an amazing language, it's like JavaScript on steroids. You have the best of the 2 worlds: the flexibility of JavaScript and static type checking. This makes it feel almost like a real professional language, such as C#.

However, the language has a weakness: lack of mechanisms for runtime type checks.

What type checks I'm talking about?

I personally don't insist on adding global mandatory dynamic type check flag since it would probably bring a lot of performance overheads and a substantial complexity for the compiler, even if the compiler could optimize the code by skipping type checks as much as possible. But I would like to have one particular feature: instanceof SomeInterface. Yes, checking if the value is an "instance" of some interface.

Idiomatic "instanceof SomeInterface"?

Many TypeScript codebases heavily utilize interfaces, especially for data structures (models, DTOs, etc). The problem with interfaces is that during runtime you can't idiomatically and easily check if some value is an instance of some interface. Such thing is possible, for example, in PHP, C# and Dart. This is somewhat understandable, because interfaces are not classes, they are just structure definitions that are only used and validated during the compile time:

interface SomeInterface { prop1: string; prop2: string; } const a: SomeInterface = { prop1: "First", prop2: "Second", } // Will not compile at all because 'SomeInterface' only refers to a type, but is being used as a value here if (a instanceof SomeInterface) { // do something }

However, even in vanilla JavaScript you can have an idiomatic way to validate the object structure by overriding the default behavior of the operator instanceof:

class User { name = ""; age = 0; static [Symbol.hasInstance](instance) { return typeof instance?.name === "string" && typeof instance?.age === "number"; } } const incompleteUser = { name: "Bob", }; const completeUser = { name: "Bob", age: 20, }; console.log(null instanceof User); // false, we also handle null with ?. operator console.log(incompleteUser instanceof User); // false, because age is undefined console.log(completeUser instanceof User); // true, despite that it's not a real instance of User

So, my suggestion is that when the TypeScript compiler generates the JS code it can generate class SomeInterface with static method [Symbol.hasInstance](instance) that will have the logic for the structure check. If the interface is a generic it can generate a separate class for each used combination of generic params. So every idiomatic TS instanceof check will be an idiomatic JS instanceof check. The class SomeInterface can still be prohibited to be used as a class and can still be exposed only as an interface in the Typescript code.

I understand the downsides of this approach, it will be slow for huge or / and deeply structured interfaces. But hey, it's not the same story as Records & Tuples, since it will not affect the code that doesn't use this feature. Also it can be confusing when debugging the code.

What about optional global runtime type checks?

This one seems a much taller order than the instanceof check. As I stated, I'm not insisting on that but this feature will bring the language to a new level. For example, Dart language prevents any illegal type castings while supporting both static and dynamic typing.

That being said, I'll not get surprised if this one will be rejected. It has several issues:

Conclusion

TypeScript could benefit from limited runtime type checks mechanisms, such as instanceof SomeInterface. Global runtime checks seem impractical due to the way JavaScript is designed.

However, despite the mentioned shortcomings, TypeScript remains one of the best languages for large and complex frontend/backend projects.





Read previous