Static typing is what can provide better tooling for our development process. While writing JavaScript, the most that IDEs and text editors can do is syntax highlight and provide some basic autocompletion suggestions based on sophisticated static analysis of our code. This means that we can only verify that we haven't made any typos by running the code.
In the previous sections, we described only the new features provided by ECMAScript expected to be implemented by browsers in the near future. In this section, we will take a look at what TypeScript provides in order to help us be less error-prone and more productive. At the time of writing this book, there were no plans to implement built-in support for static typing in the browsers.
The TypeScript code goes through intermediate preprocessing that performs the type checking and drops all the type annotations in order to provide valid JavaScript supported by modern browsers.
Just like Java and C++, TypeScript allows us to explicitly declare the type of the given variable:
let foo: number = 42;
The preceding line defines the variable foo
in the current block using the let
syntax. We explicitly declare that we want foo
to be of the type number
and we set the value of foo
to 42
.
Now, let's try to change the value of foo
:
let foo: number = 42; foo = '42';
Here, after the declaration of foo
, we will set its value to the string '42'
. This is a perfectly valid JavaScript code; however, if we compile it using the TypeScript's compiler, we will get:
$ tsc basic.ts
basic.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.
Once foo
is associated with the given type, we cannot assign it values belonging to different types. This is one of the reasons we can skip the explicit type definition in case we assign a value to the given variable:
let foo = 42; foo = '42';
The semantics of this code will be the same as the one with the explicit type definition because of the type inference of TypeScript. We'll further take a look at it at the end of this chapter.
All the types in TypeScript are subtypes of a type called any
. We can declare variables belonging to the any
type using the any
keyword. Such variables can hold the value of any type:
let foo: any; foo = {}; foo = 'bar '; foo += 42; console.log(foo); // "bar 42"
The preceding code is a valid TypeScript, and it will not throw any error during compilation or runtime. If we use the type any
for all of our variables, we will be basically writing the code with dynamic typing, which drops all the benefits of the TypeScript's compiler. That's why we have to be careful with any
and use it only when it is necessary.
All the other types in TypeScript belong to one of the following categories:
Most of the primitive types in TypeScript are the ones we are already familiar with in JavaScript: Number, String, Boolean, Null, and Undefined. So, we will skip their formal explanation here. Another set of types that is handy while developing Angular applications is the Enum types defined by users.
The Enum types are primitive user-defined types that, according to the specification, are subclasses of Number. The concept of enums exists in the Java, C++, and C# languages, and it has the same semantics in TypeScript-user-defined types consisting of sets of named values called elements. In TypeScript, we can define an enum using the following syntax:
enum STATES { CONNECTING, CONNECTED, DISCONNECTING, WAITING, DISCONNECTED };
This will be translated to the following JavaScript:
var STATES; (function (STATES) { STATES[STATES["CONNECTING"] = 0] = "CONNECTING"; STATES[STATES["CONNECTED"] = 1] = "CONNECTED"; STATES[STATES["DISCONNECTING"] = 2] = "DISCONNECTING"; STATES[STATES["WAITING"] = 3] = "WAITING"; STATES[STATES["DISCONNECTED"] = 4] = "DISCONNECTED"; })(STATES || (STATES = {}));
We can use the enum type as follows:
if (this.state === STATES.CONNECTING) { console.log('The system is connecting'); }
In this section, we will take a look at the Array types and Function types, which belong to the more generic class of Object types. We will also explore how we can define classes and interfaces. Tuple types were introduced by TypeScript 1.3, and their main purpose is to allow the language to begin typing the new features introduced by ES2015, such as destructuring. We will not describe them in this book. For further reading, you can take a look at the language's specification at http://www.typescriptlang.org.
In TypeScript, arrays are JavaScript arrays with a common element type. This means that we cannot have elements from different types in a given array. We have different array types for all the built-in types in TypeScript, plus all the custom types that we define.
We can define an array of numbers as follows:
let primes: number[] = []; primes.push(2); primes.push(3);
If we want to have an array, which seems heterogeneous, similar to the arrays in JavaScript, we can use the type reference to any
:
let randomItems: any[] = []; randomItems.push(1); randomItems.push('foo'); randomItems.push([]); randomItems.push({});
This is possible since the types of all the values we're pushing to the array are subtypes of the any
type and the array we've declared contains values of the type any
.
We can use the array methods we're familiar with in JavaScript with all the TypeScript Array types:
let randomItems: any[] = []; randomItems.push('foo'); randomItems.push('bar'); randomItems.join(''); // foobar randomItems.splice(1, 0, 'baz'); randomItems.join(''); // foobazbar
We also have the square-brackets operator that gives us random access to the array's elements:
let randomItems: any[] = []; randomItems.push('foo'); randomItems.push('bar'); randomItems[0] === 'foo' randomItems[1] === 'bar'
We're already familiar with how to define a new function in JavaScript. We can use function expression or function declaration:
// function expression var isPrime = function (n) { // body }; // function declaration function isPrime(n) { // body };
Alternatively, we can use the new arrow function syntax:
var isPrime = n => { // body };
The only thing TypeScript alters is the feature to define the types of the function's arguments and the type of its return result (i.e. the function's signature). After the compiler of the language performs its type checking and transpilation, all the type annotations will be removed. If we use function expression and assign a function to a variable, we will be able to define the variable type in the following way:
let variable: (arg1: type1, arg2: type2, ..., argn: typen) => returnType
Consider the following example:
let isPrime: (n: number) => boolean = n => { // body };
If we want to define a method in an object literal, we can do it in the following way:
let math = { squareRoot(n: number): number { // ... } };
In the preceding example, we defined an object literal with method called squareRoot
using the ES2015 syntax.
In case we want to define a function that produces some side effects instead of returning a result, we can declare it's return type as void
:
let person = { _name: null, setName(name: string): void { this._name = name; } };
TypeScript classes are similar to what ES2015 offers. However, it alters the type declarations and adds more syntax sugar. For example, let's take the Human
class we defined earlier and make it a valid TypeScript class:
class Human { static totalPeople = 0; _name: string; constructor(name) { this._name = name; Human.totalPeople += 1; } get name() { return this._name; } set name(val) { this._name = val; } talk() { return `Hi, I'm ${this.name}!`; } }
There is no difference between the current TypeScript definition with the one we already introduced; however, in this case, the declaration of the _name
property is mandatory. Here is how we can use the class:
let human = new Human('foo'); console.log(human._name);
Similarly, for most conventional object-oriented languages that support classes, TypeScript allows a definition of access modifiers. In order to deny direct access to the _name
property outside the class it is defined in, we can declare it as private:
class Human { static totalPeople = 0; private _name: string; // ... }
The supported access modifiers by TypeScript are as follows:
Access modifiers are a great way to implement Angular services with good encapsulation and a well-defined interface. In order to understand it better, let's take a look at an example using the class' hierarchy defined earlier, which is ported to TypeScript:
class Human { static totalPeople = 0; constructor(protected name: string, private age: number) { Human.totalPeople += 1; } talk() { return `Hi, I'm ${this.name}!`; } } class Developer extends Human { constructor(name: string, private languages: string[], age: number) { super(name, age); } talk() { return `${super.talk()} And I know ${this.languages.join(', ')}.`; } }
Just like ES2015, TypeScript supports the extends
keyword and desugars it to the prototypal JavaScript inheritance.
In the preceding example, we set the access modifiers of the name
and age
properties directly inside the constructor function. The semantics behind this syntax differs from the one used in the previous example. It has the following meaning: define a protected property called name
of the type string
and assign the first value passed to the constructor call to it. It is the same for the private age
property. This saves us from explicitly setting the value in the constructor itself. If we take a look at the constructor of the Developer
class, we can see that we can use the mixture between these syntaxes. We can explicitly define the property in the constructor's signature, or we can only define that the constructor accepts a parameters of the given types.
Now, let's create a new instance of the Developer
class:
let dev = new Developer('foo', ['JavaScript', 'Go'], 42); dev.languages = ['Java'];
During compilation, TypeScript will throw an error telling us that: Property languages is private and only accessible inside the class "Developer". Now, let's see what will happen if we create a new Human
class and try to access its properties from outside its definition:
let human = new Human('foo', 42); human.age = 42; human.name = 'bar';
In this case, we'll get the following two errors:
Property age is private and is only accessible inside the class "Human", and the Property name is a protected and only accessible inside class "Human" and its subclasses.
However, if we try to access the _name
property from inside the definition of Developer
, the compiler won't throw any errors.
In order to get a better sense of what the TypeScript compiler will produce out of a type-annotated class, let's take a look at the JavaScript produced by the following definition:
class Human { constructor(private name: string) {} }
The resulting ECMAScript 5 will be as follows:
var Human = (function () { function Human(name) { this.name = name; } return Human; })();
The defined property is added directly to the objects instantiated by calling the constructor function with the operator new
. This means that once the code is compiled, we can directly access the private members of the created objects. In order to wrap this up, access modifiers are added in the language in order to help us enforce better encapsulation and get compile-time errors in case we violate it.
Subtyping in programming languages allows us to treat objects in the same way based on the observation that they are specialized versions of a generic object. This doesn't mean that they have to be instances of the same class of objects, or that they have a complete intersection between their interfaces. The objects might have only a few common properties and still be treated the same way in a specific context. In JavaScript, we usually use duck typing. We may invoke specific methods for all the objects passed to a function based on the assumption that these methods exist. However, all of us have experienced the undefined is not a function error thrown by the JavaScript interpreter.
Object-oriented programming and TypeScript come with a solution. They allow us to make sure that our objects have similar behavior if they implement interfaces that declare the subset of the properties they own.
For example, we can define our interface Accountable
:
interface Accountable { getIncome(): number; }
Now, we can make sure that both Individual
and Firm
implement this interface by performing the following:
class Firm implements Accountable { getIncome(): number { // ... } } class Individual implements Accountable { getIncome(): number { // ... } }
In case we implement a given interface, we need to provide an implementation for all the methods defined inside it, otherwise, the TypeScript compiler will throw an error. The methods we implement must have the same signature as the ones declared in the interface definition.
TypeScript interfaces also support properties. In the Accountable
interface, we can include a field called accountNumber
with a type of string:
interface Accountable { accountNumber: string; getIncome(): number; }
We can define it in our class as a field or a getter.
Interfaces may also extend each other. For example, we may turn our Individual
class into an interface that has a social security number:
interface Accountable { accountNumber: string; getIncome(): number; } interface Individual extends Accountable { ssn: string; }
Since interfaces support multiple inheritances, Individual
may also extend the interface Human
that has the name
and age
properties:
interface Accountable { accountNumber: string; getIncome(): number; } interface Human { age: number; name: number; } interface Individual extends Accountable, Human { ssn: string; }
In case the class's behavior is a union of the properties defined in a couple of interfaces, it may implement all of them:
class Person implements Human, Accountable { age: number; name: string; accountNumber: string; getIncome(): number { // ... } }
In this case, we need to provide the implementation of all the methods declared inside the interfaces our class implements, otherwise, the compiler will throw a compile-time error.