TypeScript tutorial: Basics and typing
During the midst of September, version 2.0 of TypeScript was released, which does not sound too alarming, but it is actually quite a milestone in the history of this JavaScript superset and also a much more refined and sophisticated product. Its refinement can be expressed through the newest and probably most important feature, which is the optional static typing.
According to the project’s manager, Anders Hejlsberg, the new implementations enable developers to work on a larger scale in terms of JavaScript development. The language has an open source history; it has been under the influence of a communal development since 2012, still is and probably will continue to be in the future. It uses transpiling for ECMAScript 3 but can also transpile to newer versions of ECMAScript optionally, therefore guaranteeing a working JavaScript on all browsers. Finally, the package is completed by the means of Tooling, which fits neatly into the ecosystem of Node.js and npm.
All established IDEs and Editors can profit from TypeScript compiler architecture, which allows for plug-ins with code completing and refactoring capabilities. More importantly, it is now possible to handle much larger JavaScript code bases with the existing tools. Even the industry giant, Google, uses TypeScript for their popular framework Angular, a fact that implies the long lifespan of the project. Now let us move on to the main feature of TypeScript, the optional static typing. This feature provides a row of basic types, which are shown in action down below, in Listing 1:
Listing 1: Base Types let isDone: boolean = false; let decimal: number = 6; let color: string = 'berlin'; // Getype Arrays auf zwei Arten let aArray: number[] = [1, 2, 3]; let aGenericArray: Array = [4,5,6]; enum Color {Red, Green, Blue}; let c: Color = Color.Green;
Ipredition to the classic, boolean values boolean, string for Strings and number for numeric values (with a decimal, hexadecimal, binary and octane format), it is also possible to formulate Arrays in two different ways: in a quite traditional way, for example with an array made of strings with:
let aArray: string[] = ['Hello', 'World', '!'];
Or with Generics:
let aArray: Array<string> = ['Hello', 'World', '!'];
One of the last basic types is enum, which can be used to define enumerations. It is set up in an ascending order, starting from 0. Alternatively, the numeric values themselves can be assigned.
Migrating from JavaScript to TypeScript can sometimes lead to the illumination of errors. This concerns the typing of the code base and special cases with types that are not clearly resolved. There are also some special types that simplify integration with JavaScript. Listing 2 first shows any type, which can be used whenever a variable can have values of different types.
Listing 2: Special types // Can contain any type let notSure: any = 4; notSure = 'I am undecided'; let tuple: [string, number]; tuple = ["Hello", 42];
Arrays can also contain different types, such as let tuple:[string, number];. It should be noted that a sequence is associated with this and that as of index 2, each of the two types is permitted. The following assignments are therefore correct:
tuple = ["Hello", 42];
tuple = ["Hi", 42, 27, "Zoe"];
Version 2.0 introduced the new type never, which is utilized if a method does not terminate at all. It happens quite often that this process is confused with void, which applies if a method does not result in anything. The method in Listing 3 always throws an error, so it does not terminate correctly. Due to this, it is assigned the return value, the so-called Bottom Type never, which expresses this kind of behavior. This can also be utilized for the implementation of runtime and compile checks, which check for this kind of behavior. You can find a more detailed explanation here and here.
Listing 3 // If Methods don’t have a return value: never function error(message: string): never { throw new Error(message); }
TypeScript: Why you should get it now!
The best argument for purchasing TypeScript is the optional static typing. In fact, it is quite refined and the compiler is very reliable and fast. Also, types can be created easily by using Interfaces. Listing 4 showcases such a Robot Interface. It can contain both properties and function signatures. The so-called optional properties are a special feature. Just like the property model, they are marked with ? and they can exist either in a concrete form or not.
Listing 4: A simple interface interface Robot { name: string; model?: number; isDiscontinued?: boolean; sayName(): void; }
A class can then implement this interface and the compiler checks if the contract is being adhered to. This does show a great advantage of working with static typing because it is quite difficult and can be overwhelming to guarantee compliance only through a high-test coverage if you have a rather huge code base. TypeScript, on the other hand, is outfitted with a security net in its compiler, especially for this case. The class C3PO is a valid implementation, despite the missing optional properties model and isDiscontinued.
Listing 5: Deducted class class C3PO implements Robot { name: string; constructor(name: string) { this.name = name; } sayName(): void { alert(this.name); } }
It is more interesting to see that it can be the case where properties can be missing, but should not be an excess of them to implement an interface. The following code would not be valid:
let wrongC3PO: Robot = {name : 'Falsum', sayName : function(){}, excess : true};
For a true object orientation, one key feature is still missing; the data encapsulation. For this purpose, TypeScript contains the modifiers public, protected, private and readonly (Listing 6), some of which are known from other languages. The first three behave as you would expect it from languages like Java. Only the package visibility with protected, behaves differently, because of the missing package structure in JavaScript. Only subclasses inheriting from a class can access a property marked with protected. The modifier readonly is the counterpart to final in other languages. Properties provided with readonly can be set exactly once when an object is created.
Listing 6: Modifier class TinyThing { name: string; protected weight: number; private age: number; readonly color?: string; }
Become part of our International JavaScript Community now!
LEARN MORE ABOUT iJS:
Characteristics of typing with TypeScript
The type system of TypeScript is basically not “sound” build. This means that in special cases it will not generate an error because it could not determine a correct typing during the compile runtime. This has to do with JavaScript’s nature, which often utilizes such special cases. For a more detailed explanation of these cases, look here. Otherwise, the type system has four properties (Listing 8) which are also optional.
Listing 8: Defining types via interface interface LameFunc { (): void; } let funcType: LameFunc; let funcType2: (robot)=>void; funcType = printRobotInfo; // Error: Wrong function type funcType2 = printRobotInfo;
The compiler does not “complain” if no typing is used. In combination with its structural typing, there does exist a quite nice side effect: the compiler is able to derive the type correctly in almost all cases. This property is also known as duck typing. The local type inference is also interesting and quite powerful. If a newly created object is assigned to a non-typed variable, e.g. with c3po = new C3PO(‘c1po);
then the compiler usually knows already that c3po does contain an object of type C3PO. The last property is the contextual typing. In this case, the compiler can determine the type based on the circumstances. Thus it knows that in window. onmousedown, a mouse event is passed to the function that has (not) certain properties (Listing 7).
Listing 7 // optional let myRobot = {name: 'Arnold', sayName: function(){}}; // structural printRobotInfo(myRobot); // local let c3po = new C3PO('c1po'); c3po = 42; // error // contextual window.onmousedown = function(mouseEvent) { console.log(mouseEvent.buton); // <- Error };
When we do need function types
JavaScript has always been a functional language and it is only logical to introduce types for functions. These can even be defined in TypeScript via an interface (Listing 8). The parameter list is given in parentheses and then the return type. The type void indicates that a function has no return value. If you don’t go over the interface, use the notation with Fat Arrow: (p1, p2,…) => T… . Function types are particularly useful when callbacks are used in a code base or when callbacks are generally programmed functionally. Both cases are often found in JavaScript code bases with asynchronous calls.
SEE ALSO: Implementing cross-cutting concerns in Angular 2 with TypeScript
A few words about type definition
In TypeScript, an interface can be described with both the static and the instance part simultaneously. That sounds confusing at first and, in fact, even the manual does not offer a clarification either. Let’s take a look at the example in Listing 9. The interface defines a property age on the instance side. On the static side, the constructor is defined with the keyword new. The Mortal class implements the interface, but there is an error because the compiler cannot find the new method. Why does this happen, despite the fact that there is a constructor which contains the necessary signature?
Listing 9: Interface describes static and instance page interface MortalInterface { age: number; new (age: number); } // Type Mortal provides no match for the signature 'new (age :number) :any' class Mortal implements MortalInterface { age: number; constructor(age: number) { } }
Should you create on object with let aMortal = new Mortal(42), the constructor is being called, but it is then no longer accessible, due to the fact that it does not exist as a method of the object anymore. A call like aMortal.new(42) is therefore no longer possible. It is still the case though, that the Interface MortalInterface demands the existence of such a method. An Interface should only describe either the instance page or the static one. It is still possible, that both are shown together, but the compiler does only check instance page – it is then not possible to implement a class that meets these requirements.
The definition of the static page does only make sense if a Factory is built. Listing 10 does define two interfaces, which are used to construct an object of the class Human. The HumanConstructor describes the constructors’ signature; the HumanInterface describes the properties, which should be contained within the object itself. Now, if you want to create a concrete class Human, it does implement the InstanceInterface HumanInterface and it must be assigned the same constructor like in HumanConstructor.
The factory method createHuman is expecting an object, which does meet the constructors’ specifications of HumanConstructor and, in return, outputs an object of the type HumanInterface and there you have it – both pages, static and instance, have been combined.
Listing 10: Two Interfaces for the construction of an object of the class “Human” interface HumanConstructor { new (age: number): HumanInterface; } interface HumanInterface { age: number; } class Human implements HumanInterface { age: number; constructor(age: number) { this.age = age; } } function createHuman(ctor: HumanConstructor, age: number): HumanInterface { return new ctor(age); } let dijkstra = createHuman(Human, 86);
Parameterizing classes and methods with generics
Generics are generally used to parameterize classes and methods with types. This way, it is possible to report the types of elements, which are contained in an array, to the compiler.
Array
This can be quite useful for developers because a type check does take place already during the compile time. In addition, the type of the contained element is known and can be used for IntelliSense. The following example shows a case, in which a method is being parameterized. The idea originated from a blog and has been adapted and corrected for our use here. Listing 11 does implement a generic function with the so-called variable type T (spelling ), which makes an asynchronous call to an API.
Listing 11: Implementing a generic function with the so-called “Type”-Variable “T” function getAsync(arg: T): Promise { // ERROR: Property 'id' does not exist on type 'T' return fetch(`/api/${arg.id}`) .then((response: Response) => <Promise>response.json()); }
The return type is being defined as a promise, which is supposed to output JSON in the end. Due to the fact that the objects in JavaScript are practically all in the JSON format – hence the name JavaScript Object Notation – the return values can be cast back to the given type. This works with the diamond operator:
<Promise>response.json()
Unfortunately, there is a problem here since the referred parameter arg is not limited. Therefore, the compiler cannot determine if arg has a property ID. The type is solved with any. It is here, where Generic Constraints are being applied. And for this, we are going to set up a new Interface Identity, which contains a property ID:
interface Identity { id: string; }
The signature of getAsync() is being expanded and looks like this in the end:
function getAsync(arg: T): Promise
How does this help developers? Since the getAsync () method can be parameterized by using
the type-variable, it is possible to send an arbitrary request to the API, which will in return send back the object of the specified type. This example in its entirety can be seen in Listing 12.
Listing 12: Method „getAsync()“ interface Identity { id: string; } class Movie implements Identity { id: string; } function getAsync(arg: T): Promise { return fetch(`/api/${arg.id}`) .then((response: Response) => <Promise>response.json()); } let movieToFind: Movie = { 'id' : '42' }; getAsync(movieToFind) .then(movie => { console.log(movie.id); });
The class “Movie” does implement the previously discussed Interface Identity and can, therefore, be used as s Type-Variable of getAsync(). The call of getAsync() with Movie as a type-variable and a movie-object parameter does cause the type information to be available in the then-block later on. The developer has full access to IntelliSense because movie is not of the type any, but of the type Movie.
SEE ALSO: Typescript, Angular 2 and NetBeans IDE: An unbeatable trio
ECMAScript 20XX
Besides the optional static typing, TypeScript was also developed to make future JavaScript language features much earlier available, by means of transpiling to ECMAScript 3/5. This article only showcases a few of the most anticipated ECMAScript-2016/17 language features, which are already available in TypeScript. If you wish to get a more accurate picture of what awaits you, you will find a detailed overview displayed here. It all starts with the decorators, which will get full support in ECMAScript 2018. Decorators are simple functions, which are given the parameters target,name and descriptor. Listing 13 shows a simple Decorator log, which is used to decorate methods of a class.
Listing 13: Decorators function log(target: Object, name: string, descriptor: PropertyDescriptor) { console.log(target); console.log(name); console.log(descriptor); } class Greeter { @log greet(x, y) { return x + y; } }
Functions outside of a class cannot be decorated. If you create an object of the type Greeter
with:
var greeter = new Greeter();
Then you’ll see following lines in on the console display:
Greeter {}
greet
Object {
value: [Function: greet],
writable: true,
enumerable: false,
configurable: true }
This does open up a lot of new possibilities to influence the processing of the method. This procedure is also being recommended for cross-sectional aspects. In addition to the methods, the classes can be decorated too. There you can set single properties to readonly, by setting the Descriptor writeable to false. As you can see, the possibilities are huge. For a more detailed introduction, you can follow the link here.
Another anticipated feature is Async/Await, which simplifies working with asynchronous functions, especially Promises. The idea behind this is that the programming model is structured like synchronous code, but actually runs asynchronously in the background. This feature is available as of TypeScript 1.7 when transpiling to ECMAScript 2015 and since TypeScript 2.1 is also available for ECMAScript 5. Now, let’s take a look at Listing 14, for this scenario:
Listing 14: Async/Await function getPromise(): Promise { return new Promise((resolve, reject) => { resolve("Hello Zoe and Amelie"); }); } async function afunc() { let res = await getPromise(); console.log(res); }
The function afunc is assigned the keyword async. This is necessary to be able to use await within the function. If you now call afunc with let res = afunc(); you’ll see the following displayed on the console:
Hello Zoe and Amelie
If you were to work without async/await without their keywords, the console’s output would be undefined, because the return value of the promise is not waited for.
Conclusion
Over the last few years, TypeScript has become a real alternative to JavaScript. The JavaScript for backend developers scores with optional static typing and a fast, intelligent compiler. In addition, real object-oriented development is possible with interfaces and modifiers.
For these reasons, TypeScript is destined to be used in large projects. It is rounded off by the provision of features from future JavaScript versions, most of which can be transpilied to ECMAScript 3/5 and, therefore, are fully backwards compatible with the JavaScript language standard.
Program Highlights of iJS London 2020
→ Taking your web app offline (in a good sense)
→ The journey of publishing a PWA to App Stores