Classes

TypeScript provides support for classes introduced with ES6 (ECMAScript 2015) and adds a set of features to improve object-oriented development.

class Widget {

    id: string;

    constructor(id: string) {
        this.id = id;
    }

    render() {
        console.log(`Rendering widget "${this.id}"`);
    }

}

let widget = new Widget('text1');
widget.render();

You should get the following output when executed:

Rendering widget "text1"

Properties

With ES6 you define class properties from with the class constructor:

// ES6
class Widget {

    constructor(id) {
        this.id = id;
        this.x = 0;
        this.y = 0;
    }

}

If you try compiling example above with tsc utility (TypeScript compiler) you should get the following errors:

error TS2339: Property 'id' does not exist on type 'Widget'.
error TS2339: Property 'x' does not exist on type 'Widget'.
error TS2339: Property 'y' does not exist on type 'Widget'.

The errors are raised because TypeScript requires you to define properties separately. It is needed to enable many other features TypeScript provides.

class Widget {

    id: string;
    x: number;
    x: number;

    constructor(id: string) {
        this.id = id;
        this.x = 0;
        this.y = 0;
    }
}

Properties in TypeScript can have default values:

class Widget {

    id: string;
    x: number = 0;
    x: number = 0;

    constructor(id: string) {
        this.id = id;
    }
}

Setters and Getters

TypeScript supports computed properties, which do not store a value. Instead, they provide getters and setters to retrieve and assign values in a controlled way.

TBD: describe get/set format

One of the common cases for a getter is computing a return value based on other property values:

class User {

    firstName: string;
    lastName: string;

    get fullName(): string {
        return `${this.firstName} ${this.lastName}`.trim();
    }

    constructor(firstName: string, lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

}

let user = new User('Joan', 'Doe');
console.log(`User full name is: ${user.fullName}`);

If you save this example to file script.ts, compile it and run like shown below:

tsc --target ES6 script.ts
node script.js

You should see the output with the full username as expected:

User full name is: Joan Doe

Now let's introduce a simple setter for the firstName property.

Every time a new property value is set we are going to remove leading and trailing white space. Such values as " Joan" and "Joan " are automatically converted to "Joan".

class User {

    private _firstName: string;

    get firstName(): string {
        return this._firstName;
    }

    set firstName(value: string) {
        if (value) {
            this._firstName = value.trim();
        }
    }
}

let user = new User();
user.firstName = '  Joan   ';
console.log(`The first name is "${user.firstName}".`);

The console output, in this case, should be:

The first name is "Joan".

Methods

Methods are functions that operate on a class object and are bound to an instance of that object. You can use this keyword to access properties and call other methods like in the example below:

class Sprite {

    x: number;
    y: number;

    render() {
        console.log(`rendering widget at ${this.x}:${this.y}`);
    }
    
    moveTo(x: number, y: number) {
        this.x = x;
        this.y = y;
        this.render();
    }

}

let sprite = new Sprite();
sprite.moveTo(5, 10);
// rendering widget at 5:10

Return values

class NumberWidget {

    getId(): string {
        return 'number1';
    }

    getValue(): number {
        return 10;
    }

}

You can use a void type if the method does not return any value.

class TextWidget {

    text: string;

    reset(): void {
        this.text = '';
    }

}

Method parameters

You can add types to each parameter of the method.

class Logger {
    
    log(message: string, level: number) {
        console.log(`(${level}): ${message}`);
    }

}

TypeScript will automatically perform type checking at compile time. Let's try providing a string value for the level parameter:

let logger = new Logger();
logger.log('test', 'not a number');

You should get a compile error with the following message:

error TS2345: Argument of type '"string"' is not assignable to parameter of type 'number'.

Now let's change level parameter to a number to fix compilation

let logger = new Logger();
logger.log('test', 2);

Now we should get the expected output:

(2): test

Optional parameters

By default, all method/function parameters in TypeScript are required. However, it is possible making parameters optional by appending ? (question mark) symbol to the parameter name.

Let's update our Logger class and make level parameter optional.

class Logger {
    
    log(message: string, level?: number) {
        if (level === undefined) {
            level = 1;
        }
        console.log(`(${level}): ${message}`);
    }

}

let logger = new Logger();
logger.log('Application error');

The log method provides default value automatically if level is omitted.

(1): Application error

Please note that optional parameters must always follow required ones.

Default parameters

TypeScript also supports default values for parameters. Instead of checking every parameter for undefined value you can provide defaults directly within the method declaration:

class Logger {
    
    log(message: string = 'Unknown error', level: number = 1) {
        console.log(`(${level}): ${message}`);
    }

}

Let's try calling log without any parameters:

let logger = new Logger();
logger.log('Application error');

The output, in this case, should be:

(1): Application error

Rest Parameters and Spread Operator

In TypeScript, you can gather multiple arguments into a single variable known as rest parameter. Rest parameters were introduced as part of ES6, and TypesScripts extends them with type checking support.

class Logger {

    showErrors(...errors: string[]) {
        for (let err of errors) {
            console.error(err);
        }
    }

}

Now you can provide an arbitrary number of arguments for showErrors method:

let logger = new Logger();
logger.showErrors('Something', 'went', 'wrong');

That should produce three errors as an output:

Something
went
wrong

Rest parameters in TypeScript work great with Spread Operator allowing you to expand a collection into multiple arguments. It is also possible mixing regular parameters with spread ones:

let logger = new Logger();
let messages = ['something', 'went', 'wrong'];

logger.showErrors('Error', ...messages, '!');

In the example above we compose a collection of arguments from arbitrary parameters and content of the messages array in the middle.

The showErrors method should handle all entries correctly and produce the following output:

Error
something
went
wrong
!

Constructors

Constructors in TypeScript got same features as methods. You can have default and optional parameters, use rest parameters and spread operators with class constructor functions.

Besides, TypeScript provides support for automatic property creation based on constructor parameters. Let's create a typical User class implementation:

class User {

    firstName: string;
    lastName: string;

    get fullName(): string {
        return `${this.firstName} ${this.lastName}`.trim();
    }

    constructor(firstName: string, lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

}

Instead of assigning parameter values to the corresponding properties we can instruct TypeScript to perform an automatic assignment instead. You can do that by putting one of the access modifiers public, private or protected before the parameter name.

You are going to get more details on access modifiers later in this book. For now, let's see the updated User class using automatic property assignment:

class User {

    get fullName(): string {
        return `${this.firstName} ${this.lastName}`.trim();
    }

    constructor(public firstName: string, public lastName: string) {
    }

}

let user = new User('Joan', 'Doe');
console.log(`Full name is: ${user.fullName}`);

TypeScript creates firstName and lastName properties when generating JavaScript output. You need targeting at least ES5 to use this feature.

Save example above to file script.ts then compile and run with node:

tsc script.ts --target ES5
node script.js

The output should be as following:

Full name is: Joan Doe

You have not defined properties explicitly, but fullName getter was still able accessing them via this. If you take a look at the emitted JavaScript you should see the properties are defined there as expected:

// ES5
var User = (function () {
    function User(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    Object.defineProperty(User.prototype, "fullName", {
        get: function () {
            return (this.firstName + " " + this.lastName).trim();
        },
        enumerable: true,
        configurable: true
    });
    return User;
}());
var user = new User('Joan', 'Doe');
console.log("Full name is: " + user.fullName);

Now you can also switch to ES6 target to see how TypeScript assigns properties:

tsc script.ts --target ES6

The generated JavaScript, in this case, is even smaller and cleaner:

// ES6
class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    get fullName() {
        return `${this.firstName} ${this.lastName}`.trim();
    }
}
let user = new User('Joan', 'Doe');
console.log(`Full name is: ${user.fullName}`);

Inheritance

One of the important TypeScript features is the class inheritance that enables OOP patterns for developers. Under the hood TypeScript is using the same extends syntactic sugar when targeting ES6 JavaScript, and prototypical inheritance wrappers when generating output in ES5.

We can refer to animals as a classic example of class-based programming and inheritance.

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    makeSound() {
        console.log('Unknown sound');
    }
}

You have created a basic Animal class that contains a name property and makeSound method. That translates to ES5 as following:

// ES5
var Animal = (function () {
    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.makeSound = function () {
        console.log('Unknown sound');
    };
    return Animal;
}());

Now you can create a Dog implementation that provides a right sound:

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }
    makeSound() {
        console.log('Woof-woof');
    }
}

Please note that if you have a constructor in the base class, then you must call it from all derived classes. Otherwise, TypeScript should raise a compile-time error:

error TS2377: Constructors for derived classes must contain a 'super' call.

Here's how a Dog gets converted to ES5:

var Dog = (function (_super) {
    __extends(Dog, _super);
    function Dog(name) {
        return _super.call(this, name) || this;
    }
    Dog.prototype.makeSound = function () {
        console.log('Woof-woof');
    };
    return Dog;
}(Animal));

Now let's add a Cat implementation with its sound and test both classes:

class Cat extends Animal {
    constructor(name: string) {
        super(name);
    }
    makeSound() {
        console.log('Meow');
    }
}

let dog = new Dog('Spot');
let cat = new Cat('Tom');

dog.makeSound();
cat.makeSound();

Once the code compiles and executes you should get the following output:

Woof-woof
Meow

Access Modifiers

TypeScript supports public, private and protected modifiers for defining accessibility of the class members.

Public

By default, each member of the class is public so that you can omit it. However, nothing stops you from declaring public modifier explicitly if needed:

class User {
    public firstName: string;
    public lastName: string;

    public speak() {
        console.log('Hello');
    }

    constructor(firstName: string, lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Now if you compile example above to JavaScript you should see the following:

var User = (function () {
    function User(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    User.prototype.speak = function () {
        console.log('Hello');
    };
    return User;
}());

Private

You mark a member as private when it should never be accessed from outside of its containing class. One of the most common scenarios is creating private fields to hold values for properties. For example:

class User {
    private _firstName: string;
    private _lastName: string;

    get firstName() {
        return this._firstName;
    }

    get lastName() {
        return this._lastName;
    }

    constructor(firstName: string, lastName: string) {
        this._firstName = firstName;
        this._lastName = lastName;
    }
}

The class we have created above allows setting user's first and last name only from within the constructor.

If you try changing name properties from outside the class, TypeScript will raise an error at compile time:

let user = new User('John', 'Doe');
user.firstName = 'Rob';
// error TS2540: Cannot assign to 'firstName' because it is a constant or a read-only property.

Protected

The protected modifier restricts member visibility from outside of the containing class but provides access from the derived classes.

Let's start with base Page class implementation:

class Page {

    protected renderHeader()    { /* ... */ }
    protected renderContent()   { /* ... */ }
    protected renderFooter()    { /* ... */ }

    render() {
        this.renderHeader();
        this.renderContent();        
        this.renderFooter();
    }
}

We created a Page class that has public method render. Internally render calls three separate methods to render header, content and footer of the page. These methods are not available from the outside the the class.

Now we are going to create a simple derived AboutPage class:

class AboutPage extends Page {

    private renderAboutContent() { /* ... */ }

    render() {
        this.renderHeader();
        this.renderAboutContent();
        this.renderFooter();
    }

}

As you can see the AboutPage defines its render method that calls renderHeader and renderFooter in parent class but puts custom content in the middle.

You can also use protected modifier with class constructors. In this case, the class can be instantiated only by the derived classes that extend it. That becomes handy when you want to have properties and methods available for multiple classes as a base implementation, but don't want a base class to be instantiated outside its containing class.

For example

class Page {
    protected constructor(id: string) {
        // ...
    }

    render() { /* base render */ }
}

class MainPage extends Page {
    constructor(id: string) {
        super(id);
    }

    render() { /* render main page */ }
}

class AboutPage extends Page {
    constructor(id: string) {
        super(id);
    }

    render() { /* render about page */ }
}

let main = new MainPage('main');
let about = new AboutPage('about');

You can create instances of MainPage and AboutPage both having access to protected members of the Page class. However, you are not able creating an instance of the Page class directly.

let page = new Page(); 
// error TS2674: Constructor of class 'Page' is protected and only accessible within the class declaration.

Readonly modifier

One of the common ways to create a read-only property in many object-oriented programming languages is by having a private local variable with a getter only.

class Widget {

    private _id: string;

    get id(): string {
        return this._id;
    }

    constructor(id: string) {
        this._id = id;
    }
}

let widget = new Widget('textBox');
console.log(`Widget id: ${widget.id}`);
// Widget id: textBox

You can also make properties read-only by using the readonly keyword. That reduces repetitive typing when dealing with many read-only properties, and greatly improves overall code readability.

Let's update the previous example to use readonly:

class Widget {
    readonly id: string;

    constructor(id: string) {
        this.id = id;
    }
}

If you try changing the value of the property outside of the constructor TypeScript will raise an error:

let widget = new Widget('text');
widget.id = 'newId';
// error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

You can provide default values for read-only properties only in two places: property declaration and constructor.

class Widget {
    readonly id: string;
    readonly minWidth: number = 200;
    readonly minHeight: number = 100;

    constructor(id: string) {
        this.id = id;
    }
}

let widget = new Widget('text');
widget.minWidth = 1000;
// error TS2540: Cannot assign to 'minWidth' because it is a constant or a read-only property.