<<

An overview of Microsoft TypeScript

Hilde Verbeek

January 27, 2020

1 Introduction

TypeScript is a language developed by Microsoft and first released in 2012, that is meant as an improved version of JavaScript. TypeScript is a superset of JavaScript, meaning ev- ery JavaScript program is also a valid TypeScript program. TypeScript programs compile to JavaScript, so can be run in web browsers making them useful for the same purposes as JavaScript, with web development at its core.[1] TypeScript was created with several similar languages for the same purposes existing, such as CoffeeScript and Google’s Dart. However, TypeScript has become one of the most popular languages for web development over the years, ranking as the tenth most used language in Stack Overflow’s 2019 survey.[2] The language was developed after Microsoft developers felt the need for a better alternative to JavaScript. The language aims to mitigate the shortcomings of JavaScript, mainly with regards to its (which is where the languages gets its name). The main feature to achieve this, is the addition of a static type checker, but besides this, TypeScript adds many other features to improve the quality when one would normally use JavaScript. This report will look at some of the features of TypeScript, and analyze and evaluate the usefulness of the type system. It will end with some of the advantages and drawbacks to using TypeScript over pure JavaScript or other alternatives. The entire language specification of TypeScript is available online. This report royally refers to it for certain details, whereas some others are the result of experimenting with the TypeScript compiler.[3]

2 Language Features

Because TypeScript is a syntactic superset of JavaScript, its possibilities and purposes are much the same. TypeScript-specific features are mostly meant to improve the development process of writing a typical JavaScript application, with a focus on adding a static type checker, as well as generally extending and improving JavaScript’s type system. Other features include to improve existing language concepts.

2.1 TypeScript is classified as a multi-paradigm language. Like JavaScript, it is at its core an imperative language, that contains features that allow for different paradigms. The inclusion of classes and objects allows for an object-oriented approach. TypeScript’s static type checker, as well as some other features enhance this. Furthermore, there are multiple features that allow approaches, such as functions as first-class citizens. TypeScript’s type system also allows ways to easily define algebraic data types.[4]

1 2.2 Type system TypeScript’s essential addition to JavaScript, and its namesake, is the static type checker intro- duced by the compiler. JavaScript’s own type system is a loosely-, dynamically-typed system that is often at the core of criticism directed at the language. TypeScript seeks to eliminate these problems, by introducing a compile-time static type checker, using a system commonly known as . The is allowed to add (optional) type annotations to variables, constants, parameters and return values; if no type annotation is given, the compiler attempts to infer the type from context. Using the provided or inferred types, the compiler statically checks the usage of values, and raises errors upon misuse. The enforced type system is more strict and safe than JavaScript’s dynamic typing system: for example, it is strongly typed, and as such does not allow for every value to be used in every context. The type system is further detailed in the next section.

2.3 Scalability One goal of TypeScript and specifically its static type checker, is to improve the scalability of applications. There are multiple ways this is helped: for one, static type checking prevents the misuse of functions with regards to parameter and return types, which becomes more and more likely in large applications, especially in teams. In pure JavaScript, this kind of type errors can often goes uncaught. Furthermore, TypeScript’s compiler allows for the creation of declaration files (similar to /C++’s header files) that only describe the types of functions and values exported by a module. These declaration files allow for efficient module or library development, as the applications using these modules can refer to the declaration files during type checking.

3 Type System

3.1 The JavaScript type system To understand TypeScript’s static typing, it is first important to understand how JavaScript itself handles types. Remember that a TypeScript program compiles to JavaScript, so Type- Script is actually (sort of) subjected to these rules as well; however, it adds guards at compile time to ensure types are not used in “unsafe” ways that will cause weird or unexpected results when run by a JavaScript interpreter.

3.1.1 Primitive and object types JavaScript has a limited amount of types. Some are straight-forward primitive types such as number, string and boolean. The types null and undefined correspond to their equally- named values. The type object is applied to any JavaScript object, including arrays and user- defined types constructed with new. This means that there is no way to distinguish between object types at runtime; the reasons and implications of this become clear later on.[5]

3.1.2 Weak typing and type coercion A typing feature of JavaScript, that is commonly referred to as “weak typing”, is one of the problematic features that TypeScript attempts to mitigate. Weak typing means that in any application of a value, the interpreter will attempt to find a way to use it, and produce a value. This process is called type coercion. An example is the expression "3" * "4": there is no way to multiply strings, so the interpreter casts the values to numbers, and the result is 12 (a number).

2 This is likely not the intention of the programmer, as they would probably actually provide values of type number if it was. In case one of the operands cannot be properly parsed as a number, such as in "3" * "foo", it will become NaN, and so will the outcome of the expression. This could cause serious problems in a program. While the result of this expression should still be clear, there are much worse examples where type coercion produces extremely unexpected and arbitrary-seeming results. [6]

3.1.3 Duck typing A typing pattern that is certainly not unique to JavaScript, is called “duck typing”. After the poet James Whitcomb Riley, who wrote “When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”, duck typing means that any object can be treated as a desired type, as long as it more or less exhibits the behavior of said type. To call a function, or access a property of an object, the object’s type or structure does not matter as long as it contains the requested property.[7] Consider the following example: function Person(name) { this.name = name; } Person.prototype.greet = function() { console.log("Hello, " + this.name); } var people = []; people.push(new Person("Amy")); people.push(new Person("Josh")) people.push({greet: function() {console.log("I’m a duck!");}}); for(var person of people) person.greet();

This code will run fine, even though the third object is not defined as a Person and has its own greet method instead. For each of the objects, JavaScript will simply check if it has a greet method and run it, regardless of how it was defined. In certain other languages, especially statically-typed ones, such an approach would not work, as it would require a shared definition of the method, for instance in a superclass. The behavior can be used for good and bad: one one hand, it allows for a very simple definition of interfaces (without explicitly defining the interface beforehand) to use a strategy pattern. However, the lack of static type checking makes it much more prone to runtime errors (eg. when some of the methods are not defined by an implementation) and occasionally harder to understand as programmer.[8] On top of that, the lack of static definitions and protections (eg. non-public interfaces and unoverridable methods) make it more prone to malicious usage.

3.2 Overview of TypeScript types TypeScript introduces a system of static type checking, that is performed as the code is compiled to JavaScript, in order to make it more “developer-proof”. The static checker is subject to much stricter rules than pure JavaScript: the previous example of the expression "3" * "4" will produce a compiler error: The right-hand side of an arithmetic operation must be of type ‘any’, ‘number’, ‘bigint’ or an enum type. However, it should be noted that despite the compiler error it still produces output, as it is still valid JavaScript code that will have no trouble running.

3 We will look at a subset of typing features and concepts to better understand how TypeScript handles it.

3.2.1 Type annotations and TypeScript checks type correctness from the bottom up. On the lowest level, the type of simple values is often obvious: "foo" is obviously a string, and 42 is obviously a number. From here on out, the types of more complex expressions can be inferred: for instance, the result of a number added to a number, is obviously itself a number. This process is called type inference. When declaring functions, variables and constants, the programmer can choose to supply a type annotation, to declare the type of said value. If the type annotation is omitted, in many cases it can and will still be inferred by the compiler, from its initial value. Take the following examples: var some_string: string = "foo"; // OK var some_number = 42; // type number is automatically inferred var wont_work: string = true; // err: boolean cannot be assigned to string var some_value: any = 15; // any value can be assigned to the any type

To understand the type system, it is important to understand the notion of assignability. In broad terms, a value v is assignable to a variable of type τ, if v can be treated as a value of type τ. The third example above does not work because booleans are not assignable to string, and therefore the compiler will throw an error. The any type is a bit of a special case: every value is assignable to any, and any value with type any is assignable to every other type. This means the earlier example of multiplying two strings can actually be permitted in TypeScript, as long as both operands are defined with type any. The exact definition of assignability is rather complex, and is defined in the TypeScript language specification, but most of it should intuitively arise from the examples.

3.2.2 Primitive types The primitive types as shown before, besides any, are equivalent to those in JavaScript. Besides these, there exists the void type for functions without a return type (and no use as a value itself), and user-defined enum types. Enums can be defined by the developer using the keyword enum like in many other languages. Enum values simply compile to integers, as JavaScript does not support enums. Moreover, enum types are assignable to number and vice versa.

3.2.3 Objects and interfaces With objects it gets more interesting. The type of an object is defined by its members, by their name and types. The object {foo: 3, bar: false} gets a type that is written as {foo: number; bar: boolean;}, ie. any object that contains a property foo of type number and a property bar of type boolean. Besides properties, members can also be call signatures (ie. when the object is called like a function, its parameters and return value), index signatures (when the object is indexed like an array) or construct signatures (when the object is called using new). An object type can be defined as an interface, allowing it to be reused easily and to be implemented by classes. The previous example could be used as follows: interface SomeType { foo: number; bar: boolean;

4 } var SomeObject: SomeType = {foo: 3, bar: false};

An object o is assignable to an object type τ if every member of τ is defined by o, with the same name and type. o may in this case have members that are not defined by the type, but these cannot be used after the assignment: var obj: SomeType = {foo: 5, bar: true, baz: "hello"}; // OK console.log(obj.baz); // error: Property ’baz’ does not exist on SomeType

Properties can have a defined visibility using the keywords public, private and protected, like in OOP languages. These only really make sense when used in a class definition, as men- tioned later on. By default, members are public. Furthermore, properties can be either required (default) or optional (with a question mark after their name). Optional properties do not have to exist in objects of the type, but can still be accessed.

3.2.4 Functions Function types are defined by the types of their parameters and their return types, both of which can be annotated inside the function definition (however, only the return type can be inferred from the function body, due to the inferring process being bottom-up). There are several ways to declare the type of a function; one is using a call signature in an object type. There exists function overloading in TypeScript, simply by giving multiple declarations of the function. However, there can only be one implementation per function, meaning the developer still has to determine the overloaded variant by hand at runtime.

3.2.5 Union and intersection types TypeScript has two ways of combining types. The first one is union types: where a value can be one of two (or more) types. Some examples: var x: number | string; // x can be eiter number or string x = 3; // OK x = "foo"; // OK x = true; // error: boolean is not assignable to number | string x.charAt(0); // error: has no function charAt, even though string does

From the last example, it can be seen that the only accessible members are ones that are shown by both possible types, ie. the intersection of their members (which is where the name “union type” can get counter-intuitive). As an example, with the type {a: number; b: number} | {b: string; c: number}, it is no problem accessing the property b, as it is defined by both types. However, since the two members do have different types, the type of b will itself be number | string. Union types arise in some interesting situations: for example. the ternary conditional - tor may produce a union type if its operands are different: b ? "foo" : false will have the type string | boolean. Furthermore, an array’s type may be inferred as union if its members are of different types: [1, 2, "Fizz"] has the type (number | string)[]. It is possible to test the “actual” type of a union value using JavaScript’s typeof operator, and a “type assertion” in TypeScript, which is similar to a type cast. Note that a union type cannot be simply assigned to one of its essential types without a type assertion.

5 var x: number | string = getNumberOrString(); if(typeof x == ‘number’) { var n: number = x; // do something with n... } // etc.

Besides union types, TypeScript also has intersection types. A type t1 & t2 can take any value that is both t1 and t2 (ie. the intersection of the two types). This only really makes sense for objects, as a value cannot really be two different primitive types at once. Recall how an object may be assigned to an object type, even if it has extra members that are not described by the type. Now the type {foo: number} & {bar: string} should make sense, as any object with properties foo: number and bar: string is assignable to both types. Therefore, the type is equivalent to {foo: number; bar: string}, showing how, converse to union types and equally counterintuitive, the members of an is the union of the two types’ members. Assignability with intersection types is more or less opposite of the rules for union types. A value of type t1 & t2 is assignable to both t1 and t2, but values of type t1 or t2 are not assignable to t1 & t2 (unless the intersection is equal to one of the types, of course).

3.2.6 Classes and It is possible to define classes in TypeScript, that besides declaring the types of members, can also define a constructor and methods that instances automatically take. Classes are compiled to definitions using JavaScript’s prototype model. Besides being used to instantiate objects, classes can obviously be used as types. Classes can extend superclasses using the extends keyword as a way of subtyping; in JavaScript, this is simply compiled to the subclass copying all members of the superclass prototype. Furthermore, a class can implement an interface, meaning the compiler forces the developer to implement all members from the interface (and, in turn, any object of the class will be assignable to the interface).

3.3 Analysis of the static type system When looking at the typing system of TypeScript, it is important to always remember that dynamically it is still the same as JavaScript, by virtue of the fact that TypeScript programs are compiled to JavaScript code. Therefore, a TypeScript program is never truly free from weak typing or duck typing or other potentially problematic behavior of JavaScript’s part, and it only makes sense to look at the static typing, and how it might possibly shield a program from problems in the dynamic part. The combination of optional static typing and dynamic typing otherwise is referred to as “gradual typing”. Gradual typing is a relatively new invention, and aims to give the programmer the choice between the advantages of both static and dynamic typing. Of course, TypeScript is not entirely the same as some other gradually typed languages, as the compiled programm, which itself is JavaScript, has absolutely no knowledge of the static type checking prior to it, so “static” typed variables are actually still treated as dynamically typed by the interpreter.[9] TypeScript’s static typing discipline may best be described (and is described by its devel- opers) as “structural typing”. Structural typing, as opposed to nominal typing (as in languages like C# and Java), determines type compatibility by looking at the types’ defined structure instead of where and how they are defined. In a nominally typed language, one could define two types with the exact same properties, but objects of the two types would not be compatible be-

6 cause the types were defined in different places. Conversely, in TypeScript, the two types would be compatible, because the members are the only thing that matters for compatibility.[10] The usage of structural typing complements JavaScript’s duck typing well: while it is sim- ilar to duck typing in that it looks at member definitions to test compatibility, it allows the programmer to ensure that values still have the “correct” type. One of the problems discussed earlier with pure duck typing is that there is no way to explicitly define the expected behavior of used objects, but this is easily done in TypeScript, eg. by using an interface. The weak typing of JavaScript is compensated for by TypeScript. Unless the operands have type any, there are strict rules for how different types can be used in expressions, and unlike JavaScript, it only allows sensible behavior producing expected results. Again, this only happens at compile time. It could be said that TypeScript is strongly typed statically, and weakly typed dynamically because of this. With respect to this, something can be said about TypeScript being a “superset” of JavaScript. The intended meaning of this is that any JavaScript program can be put through the TypeScript compiler and produce itself (or an equivalent program), but due to the combination of type inference and strong static typing, some JavaScript programs would actually produce compiler errors.

4

As the main motivation to use TypeScript is its static type checker to compensate for JavaScript’s shortcomings, it is an interesting question to ask if TypeScript could be classified as “type safe”.

4.1 Definitions of type safety The actual definition of type safety is a contested topic. The most common definition, with regards to operational semantics, was formulated by Wright and Felleisen and consists of the following two criteria: 1. Well-typedness is preserved throughout the transition rules of a program;

2. An expression has no “stuck states”, ie. every expression is either evaluated to a value or has a transition to some other expression[11] A simpler definition that (roughly) boils down to the same thing, is that a type safe language is able to execute any operation meaningfully, including by raising an exception.[12] Other definitions refer to the language promoting sensible programming conventions and not producing undesirable results, or a sensible way of permitting operations with regards to a value’s type. When looking at JavaScript, it is already hard to say whether or not it is type safe. According to Wright and Felleisen’s criteria, I would say that it probably is: for every computation, the language attempts to produce a value (possibly through type coercion); if it really is not possible, it throws a runtime error. However, as we have seen before, JavaScript is not exactly good at promoting a sensible style of programming with regard to types, and some use this as an argument to JavaScript not being type safe.[13] On the other hand, one could look at the strange behavior exhibited by weak typing and similar features, and conclude that none of it is undesirable or unexpected, but rather, the programmer should just learn the rules and understand them, so they could utilize the “undesirable” behavior to their own desire.

4.2 Implications of trans-compilation to JavaScript After making a statement about JavaScript not being type safe based on its operational se- mantics, one could simply defer to the fact that every JavaScript program is also a TypeScript

7 program and say therefore TypeScript is also not type safe itself. Regardless of whether you believe this or not, I don’t think it is a very useful assertion. Of course, the operational se- mantics of TypeScript are identical to JavaScript’s, but by basing the entire verdict of type safety on this it seems as though you’d throw the entire static semantics out of the window, and essentialize the language’s quality to a dynamic aspect. This is not a useful way of evalu- ating a language, and it should be obvious that TypeScript does a lot to discourage “unsafe” programming practice regardless. Because of this, I will leave out the entire concept of “type safety”, and investigate how

1. the static type checking promotes a “safer” way of writing code; and

2. the added static semantics influences the operational semantics of JavaScript, and circum- vents undesirable behavior.

4.3 Writing “safe” code When writing a TypeScript program, the programmer has the possibility, and is encouraged to, supply type annotations for every declaration in the program (or at the very least, at places where a type cannot be inferred). Barring outside influences, and not abusing certain constructs (such as using a type assertion when the actual type is uncertain), this should every part of the program is well-typed, and does not get the type any. If this is the case, and there are no type errors at compile time, it can be reasonably expected that there will be few or no runtime errors with regard to property access (due to the structural type check; however, variables can still be null or undefined), and operators are only used on operands of “sensible” types (by the static checking being strongly typed). This obviously does not immediately mean a program is “correct”, but it should mitigate the main problems that one might have with JavaScript code exhibiting undesirable or erroneous behavior. An obvious drawback of this is that it forces the developer to write “statically typed” code, while the general purpose of gradual typing is to give them the choice wherever possible. As soon as the developer introduces a variable they want to be “dynamically” typed, it has to get the type any, and a lot of the certainty will be lost. Furthermore, there are cases where one really has no choice but to use the any type (or risk runtime errors nonetheless), such as when interpreting data that is downloaded from the internet (eg. a JSON payload). In other static languages, it is common practice to interpret downloaded data as a predefined type, and there will be a system in place to automatically test its structure against said type. However, since TypeScript’s static type definitions disappear upon compila- tion, this really isn’t possible, and you’re stuck with the possibility that a downloaded object does not have the desired structure. There exist libraries that allow this in TypeScript[14], by adding a separate runtime-check of an object’s structure, but this is definitely not ideal as it adds some development overhead.

4.4 Strict mode A useful feature of TypeScript that has been omitted so far in this report, is the compiler’s strict mode. By enabling strict mode, a number of smaller checks are utilized during compilation, encouraging well-typedness of the program. These features include: disallowing variables that are inferred to the type any (so any is only allowed when explicitly annotated), class properties always being initialized and null and undefined being unassignable to other types. The use of strict mode can further improve “type safety” by further discouraging unsafe behavior.

8 5 Reasons to (not) use TypeScript

With all the problems essential to JavaScript, and the way TypeScript deals with them, it is hard to find reasons against using it. Many refer to the static typing, and how it improves the development process. Kevin Greene promotes the static typing as it allows you to catch errors early, it is a way of “self-documenting” code, and it improves working on large codebases and in teams.[15] Others praise TypeScript for its high compatibility with pretty much all existing JavaScript libraries, or that it is easy to port existing JavaScript code to TypeScript.[16] TypeScript is open-source under the Apache License 2.0, meaning it can be used in every application free of royalty, which also makes it appealing; however, some also dismiss TypeScript for being developed by Microsoft. A straightforward but minor drawback of using TypeScript is the overhead caused by having to compile all your code: especially for small projects or testing things, it is much easier to just run native JavaScript directly, than to first compile it from TypeScript. However, in larger projects, especially with the integration into IDEs, this really does not hold up. There are many alternatives to TypeScript however, that are intended for the same purposes and also compile to JavaScript directly. Some of these alternatives are Elm, Dart and CoffeeScript. Some people may choose some other language over TypeScript, as each language is appealing for different reasons. There is also the fact that JavaScript is continually being worked on and improved, and therefore renders some of the additions of other languages (such as syntactic sugar) obsolete. Some actually criticize TypeScript for being too similar to JavaScript. They point to prob- lematic parts of JavaScript that are not fixed by TypeScript, and are therefore also present in TypeScript because TypeScript is a superset of JavaScript. An example is the usage of the this keyword, which can give some unexpected results when used in the wrong context. Jeff Walker claims on his blog that TypeScript fixes the “wrong problem”: according to him, the problem with JavaScript isn’t its loose or dynamic type system, but rather that the language is poorly designed, which isn’t fixed by just adding static typing.[17]

6 Conclusion

TypeScript is a widely adopted, versatile extension of JavaScript, that among other features adds optional static type checking to the language along the lines of gradual typing. With the static type checking as well as other of the language’s features, the developer is encouraged to write robust and type-correct code, compensating for problems arising from JavaScript’s dynamic typing that gives the language most of its criticism. TypeScript is a superset of JavaScript, meaning that every JavaScript program is also a valid TypeScript program, making it very appealing to adapt existing projects to. It is well-suited for usage alongside popular JavaScript libraries and therefore applicable in almost any application where one could also use JavaScript. However, there are some problems with JavaScript that TypeScript fails to address, some of which due to the fact that it is a superset of JavaScript. Furthermore, there are alternative languages developed along the same lines, which may be preferable over TypeScript to some, for various reasons.

9 References

[1] Ars Technica. Microsoft TypeScript: the JavaScript we need, or a solution looking for a problem? Oct. 2012. url: https://arstechnica.com/information-technology/2012/ 10/microsoft- - the- - we- need- or- a- solution- looking- for-a-problem/. [2] Stack Overflow. Stack Overflow Developer Survey 2019. url: https://insights.stackoverflow. com/survey/2019. [3] Microsoft. TypeScript Language Specification. url: https://github.com/Microsoft/ TypeScript/blob/master/doc/spec.md. [4] Daniel Rosenwasser. Algebraic data types in TypeScript. Oct. 2016. url: https://stackoverflow. com/a/39988345/3782377. [5] ECMAScript 2020 Language Specification. Jan. 2020. url: https://tc39.es/ecma262/. [6] Gary Bernhardt. Wat. url: https://www.destroyallsoftware.com/talks/wat. [7] Rubens Pinheiro Gon¸calesCavalcante. JavaScript and Duck Typing. Mar. 2018. url: https://medium.com/front-end-weekly/javascript-and-duck-typing-7d0f908e2238. [8] C´edricBeust. The Perils of Duck Typing. Apr. 2005. url: https://beust.com/weblog/ 2005/04/15/the-perils-of-duck-typing/. [9] Jeremy Siek. What is Gradual Typing. Mar. 2014. url: https://wphomes.soic.indiana. edu/jsiek/what-is-gradual-typing/. [10] Type Compatibility. url: https://www.typescriptlang.org/v2/docs/handbook/type- compatibility.. [11] A.k. Wright and M. Felleisen. “A Syntactic Approach to Type Soundness”. In: Information and Computation 115.1 (1994), pp. 38–94. doi: 10.1006/inco.1994.1093. [12] Andru. Is JavaScript a type-safe language? Mar. 2019. url: https://stackoverflow. com/a/55160101/3782377. [13] T.J. Crowder. Is JavaScript a type-safe language? Sept. 2016. url: https://stackoverflow. com/a/39642986/3782377. [14] gcanti. io-ts. Jan. 2020. url: https://github.com/gcanti/io-ts. [15] Kevin B. Greene. Surviving the TypeScript Ecosystem: Writing Type-Safe(ish) JavaScript Code. June 2018. url: https://medium.com/@KevinBGreene/surviving-the-typescript- ecosystem-writing-type-safe-ish-javascript-code-1e8375819d2e. [16] Compile to JavaScript: The 10 Best JavaScript Alternatives. Sept. 2019. url: https: / / noeticforce . com / alternative - programming - languages - that - compile - to - javascript. [17] Jeff Walker. Why TypeScript Isn’t the Answer. Feb. 2014. url: https://walkercoderanger. com/blog/2014/02/typescript-isnt-the-answer/.

10