TypeScript Object Oriented Approach


Creative Commons License
This -TypeScript Object Oriented Approach- tutorial is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License
Headlines
OO programming in JavaScript 5

Rule(s)

Example (old fashion) Temperature.js.zip 

var temperature = { // The notion of temperature
    Min: -273.15, // in Celsius
    _value: 0, // in Celsius
    _step: 0.0001,
    asCelsius: function () {
        return this._value;
    }
    // Etc.
};

Example (JavaScript 6 alternate syntax) Temperature.js.zip 

var temperature = { // The notion of temperature
    Min: -273.15, // in Celsius
    _value: 0, // in Celsius
    _step: 0.0001,
    asCelsius() { // Not so clear because confusing with a call of 'asCelsius'...
        return this._value;
    }
    // Etc.
};

Rule(s)

Example (better, but still not up-to-date) Temperature.js.zip 

var Temperature_unit = {
    Celsius: 0,
    Fahrenheit: 1,
    Kelvin: 2
};

var Invalid_temperature_exception = function (value) { // Exception type
    this._message = "Invalid temperature";
    this._value = value;
};

var Temperature = function (value, unit) {
    this.Min = -273.15; // in Celsius
    this._value = 0; // in Celsius
    switch (unit) {
        case Temperature_unit.Celsius:
            this._value = value;
            break;
        case Temperature_unit.Fahrenheit:
            this._value = (value - 32.) * 5. / 9.;
            break;
        case Temperature_unit.Kelvin:
            this._value = value + this.Min;
            break;
        default:
            throw "Illegal temperature unit";
    }
    if (this._value < this.Min) {
        throw new Invalid_temperature_exception(this._value);
    }
    this._step = 0.0001;
    this.asCelsius = function () {
        return this._value;
    };
    this.asFahrenheit = function () {
        return 9. / 5. * this._value + 32.;
    };
    // Better:
    Object.defineProperty(this, "asKelvin", {value: function () { return this._value - this.Min; },
        enumerable: true, configurable: false, writable: false}
    );
    // Etc.     
};

Example (archetype)

const Dish = function (main_ingredient) {
    this.ingredients = [];
    this.main_ingredient = main_ingredient;
    this.add_ingredient = function (ingredient) {
        this.ingredients.push(ingredient);
    };
};
const lasagna = new Dish(Symbol("Pasta"));
console.log(lasagna.main_ingredient); // 'Symbol(Pasta)' is displayed...
lasagna.add_ingredient(Symbol("Cheese"));
console.assert(lasagna.constructor === Dish);
console.assert(Object.getPrototypeOf(lasagna) === Dish.prototype); // "Prototype inheritance"...
console.log(Dish.prototype.constructor); // Constructor function (code is displayed)...
console.assert(Dish.prototype.constructor.name === "Dish");

Rule(s)

Example Temperature.js.zip 

Object.defineProperty(Temperature.prototype, "Min", {value: -273.15, enumerable: true, configurable: false, writable: false});

Temperature.prototype.asCelsius = function () {
    return this._value;
};

Temperature.prototype.asFahrenheit = function () {
    return 9. / 5. * this._value + 32.;
};

// Better:
Object.defineProperty(Temperature.prototype, "asKelvin", {value: function () {
        return this._value - this.Min;
    }, enumerable: true, configurable: false, writable: false}
);

// Etc.

Rule(s)

Example

var Pair = (first, second) => {
    this.first = first;
    this.second = second;
};
const p = new Pair('a', 'z'); // Bug: 'TypeError: Pair is not a constructor'
Pair('a', 'z'); // Bug: 'this' is 'undefined'!

Rule(s)

Example

var Pair = function (first, second) {
    this.first = first;
    this.second = second;
};
Pair.prototype.getFirst = () => {
    return this.first;
};
const p = new Pair('a', 'z'); // This works now...
window.alert(p.getFirst()); // Bug: 'this' is 'undefined'!

Example of inheritance tree (in French)

Rule(s)

Example Inheritance_polymorphism.ts.zip 

var Compte_bancaire = function (id, solde) {
    this._id = id;
    this._solde = solde;
    this._cumul_interets;
};
Compte_bancaire.Information = function () {
    return "Classe générale des comptes bancaires";
};
Compte_bancaire.prototype.id = function () {
    return this._id;
};
Compte_bancaire.prototype.solde = function () {
    return this._solde;
};
Compte_bancaire.prototype.cumul_interets = function () {
    return this._cumul_interets;
};
Compte_bancaire.prototype.mise_a_jour = function (montant) {
    this._solde += montant;
    return this._solde;
};
Compte_bancaire.prototype.taux_interet = function () {
    throw "Undefined function due to abstract nature of the class...";
};
Compte_bancaire.prototype.appliquer_taux_interet = function () {
    this._cumul_interets = this._solde * (1. + (this.taux_interet() / 100.));
};
Compte_bancaire.prototype.compareTo = function (cb) {
    return this._solde > cb._solde ? 1 : this._solde < cb._solde ? -1 : 0;
};
var Compte_cheque = function (id, solde, taux_interet, seuil) {
    Compte_bancaire.call(this, id, solde); // 'super' in Java
    this._taux_interet = taux_interet;
    this._seuil = seuil;
};
Compte_cheque.prototype = Object.create(Compte_bancaire.prototype); // Inheritance link
Compte_cheque.prototype.constructor = Compte_cheque;
Compte_cheque.Information = function () {
    return "Classe des comptes bancaires courants ou \"comptes chèque\" - rémunération à la tête du client !";
};
Compte_cheque.prototype.taux_interet = function () {
    return this._taux_interet;
};
Compte_cheque.prototype.appliquer_taux_interet = function () {
    if (this._solde > this._seuil)
        Compte_bancaire.prototype.appliquer_taux_interet.call(this); // 'super' in Java
};

Rule(s)

Example Inheritance_polymorphism.ts.zip 

console.log(Compte_bancaire.Information());
let cb = null;
try {
    cb = new Compte_bancaire("cb", 100.);
    cb.appliquer_taux_interet(); // This fails since 'Compte_bancaire' is implemented as an abstract class
    console.log(cb.cumul_interets());
} catch (e) {
    console.log(e);
}
console.log(Compte_cheque.Information());
cb = new Compte_cheque("cc", 200., 0.02, 100.);
cb.appliquer_taux_interet();
console.log(cb.cumul_interets());
OO programming in JavaScript 6 (ECMAscript 2015).

Rule(s)

Example (archetype)

class Dish_ { // From JavaScript 6...
    constructor(main_ingredient) {
        this.ingredients = [];
        this.main_ingredient = main_ingredient;
    }
    add_ingredient(ingredient) {
        this.ingredients.push(ingredient);
    };
}
const lasagna_ = new Dish_(Symbol("Pasta"));

Rule(s)

Example (static attribute in JavaScript 7)

class Temperature {
    static Min = -273.15; // in Celsius
    …
}

Rule(s)

Example

class NoLanguageSettings {
    …
    static Universe_radius = 3000;
    static Half_sphere_geometry = new THREE.SphereBufferGeometry(NoLanguageSettings.Universe_radius / 25, 30, 30, 0, Math.PI);
    static _Initializer = (() => {
        NoLanguageSettings.Half_sphere_geometry.computeBoundingSphere();
    })(); // <- Caution: automatic call...
    …
}

Example (full inheritance hierarchy) Inheritance_polymorphism.ts.zip 

class Compte_bancaire_ {
    constructor(id, solde) {
        this._id = id;
        this._solde = solde;
        this._cumul_interets;
    }
    static Information() {
        return "Classe générale des comptes bancaires";
    }
    id() {
        return this._id;
    }
    solde() {
        return this._solde;
    }
    cumul_interets() {
        return this._cumul_interets;
    }
    mise_a_jour(montant) {
        this._solde += montant;
        return this._solde;
    }
    taux_interet() {
        throw "Undefined function due to abstract nature of the class...";
    }
    appliquer_taux_interet() {
        this._cumul_interets = this._solde * (1. + (this.taux_interet() / 100.));
    }
    compareTo(cb) {
        return this._solde > cb._solde ? 1 : this._solde < cb._solde ? -1 : 0;
    }
}

class Compte_cheque_ extends Compte_bancaire_ {
    constructor(id, solde, taux_interet, seuil) {
        super(id, solde);
        this._taux_interet = taux_interet;
        this._seuil = seuil;
    }
    static Information() {
        return "Classe des comptes bancaires courants ou \"comptes chèque\" - rémunération à la tête du client !";
    }
    taux_interet() {
        return this._taux_interet;
    }
    appliquer_taux_interet() {
        if (this._solde > this._seuil) {
            super.appliquer_taux_interet();
        }
    }
}

Rule(s)

Example Inheritance_polymorphism.ts.zip 

console.log(Compte_bancaire_.Information());
let cb_ = null;
try {
    cb_ = new Compte_bancaire_("cb", 100.);
    cb_.appliquer_taux_interet(); // This fails since 'Compte_bancaire' is implemented as an abstract class...
    console.log(cb_.cumul_interets());
} catch (e) {
    console.log(e);
}
console.log(Compte_cheque_.Information());
cb_ = new Compte_cheque_("cc", 200., 0.02, 100.);
cb_.appliquer_taux_interet();
console.log(cb_.cumul_interets());
OO programming in TypeScript

Rule(s)

Example (archetype)

class Dish {
    private ingredients: Array<symbol> = new Array();
    private main_ingredient: symbol;
    public constructor(main_ingredient: symbol) {
        this.main_ingredient = main_ingredient;
    }
    public add_ingredient(ingredient: symbol) {
        this.ingredients.push(ingredient);
    };
}
const lasagna = new Dish(Symbol("Pasta"));

Classes

Rule(s)

Example N_INSEE.ts.zip 

class N_INSEE {

    constructor(private readonly _n_insee: number, private readonly _clef: number) { // Constructor is by default 'public'...
        // 'this._n_insee' is automatically inserted as private field
        // 'this._clef' is automatically inserted as private field
    }

    calcul_clef(): boolean { // Any method is by default 'public'...
        if ((97 - (this._n_insee % 97)) === this._clef)
            return true;
        else
            return false;
    }
}

const main = function () { // 'window.onload = main;'
    let n_insee: N_INSEE = new N_INSEE(1630125388055, 29);
    if (n_insee.calcul_clef()) window.alert("OK");
    else window.alert("Non OK");
}

Example (JavaScript 6 code generated by TypeScript compiler)

class N_INSEE {
    constructor(_n_insee, _clef) {
        this._n_insee = _n_insee;
        this._clef = _clef;
    }
    calcul_clef() {
        if ((97 - (this._n_insee % 97)) === this._clef)
            return true;
        else
            return false;
    }
}
let main = function () {
    let n_insee = new N_INSEE(1630125388055, 29);
    if (n_insee.calcul_clef())
        window.alert("OK");
    else
        window.alert("Non OK");
};

Getter and setter

Rule(s)

Example (use of getter)

private _focus: boolean = false;
get focus(): boolean {
    return this._focus;
}
set focus(focus: boolean) /* : void */ { // TypeScript setter rejects returned type...
    this._focus = focus;
}
…
if (object_with_focus.focus) … // Getter call is without braces...

Example (use of setter)

private _camera = Camera.General;
get camera(): Camera {
    return this._camera;
}
set camera(camera: Camera) {
    this._camera = camera;
}
…
this.camera = Camera.Embedded; // Setter call is without braces...

static property

Rule(s)

Example Inheritance_polymorphism.ts.zip 

abstract class Compte_epargne extends Compte_bancaire {
    protected static _Taux_interet: number;
    …
}

class Livret_A extends Compte_epargne {
    public static Information() {
        return "Classe des livrets A - rémunération fixée par l'Etat !";
    }
    public static Initialize() {
        Compte_epargne._Taux_interet = 0.5; // 13/02/2020
    }
}

Rule(s)

Example Inheritance_polymorphism.ts.zip 

/** Simulation of Java static initializer */
function Initialize(target: any) { // Decorator...
    target.Initialize();
}
…
@Initialize // '"experimentalDecorators": true,'
class Compte_epargne_logement extends Compte_epargne {
    public static Information() {
        return "Classe des comptes épargne logement - rémunération ratio du Livret A !";
    }
    public static Initialize() { // Launched by decorator...
        Compte_epargne._Taux_interet = Livret_A._Taux_interet * 2. / 3.; // 2/3 du taux du Livret A...
        // Compte_epargne._Taux_interet = ...; // arrondi au 1/4 point le plus proche (0.5 * 2. / 3. ==== 0.33) -> (0.25)...
    }
}

Optional property

Rule(s)

Example (attribute in class)

protected _eyes?: Array<any>;
protected _face: Array<any> | undefined; // <=> 'protected _face?: Array<any>;'
protected _mouth?: Array<any>;
…
if (this._eyes !== undefined) … // Required by the compiler with '"strict": true,'!

Example (attribute in class with simplified access based on the ? operator)

private _my_friends?: Array<Person>;
…
this._my_friends?.forEach(…); // Required by the compiler with '"strict": true,'!

Example (method in class)

protected _compute_morphing?(geometry: any): void;
…
if (this._compute_morphing) // Required by the compiler with '"strict": true,'!
    this._compute_morphing(front_geometry);

Constructor overloading

Rule(s)

Example

class Individual {
    protected readonly _birth_date: number;

    constructor(readonly birth_date: string) {
        this._birth_date = Date.parse(birth_date);
    }

    // private constructor(readonly birth_date: number) { // No other constructor is accepted, even 'private'!
    //     this._birth_date = birth_date;
    // }

    // cloning(): this { // This differs from 'Current' polymorphic type in Eiffel!
    //     return this; // Only 'this' object complies with 'this' *TYPE*!
    // }

    cloning(): Individual {
        console.log("cloning in Individual...\n");
        // return new Individual(this._birth_date); // No way because multiple constructors aren't accepted!
        return new Individual(new Date(this._birth_date).toDateString());
    }
}

Method overloading

Rule(s)

Example (default values of arguments)

private _sobel_post_processing(canvas: HTMLCanvasElement, threshold: number = 100) { …

Example (problem)

get_texture(): any { // Compilation error resulting from conflict...
    return this._textures.get(NoLanguageCharacter_texture_type.Normal);
}
get_texture(type: NoLanguageCharacter_texture_type): any { // Compilation error resulting from conflict...
    return this._textures.get(type);
}

Example (solution)

public get_texture(type: NoLanguageCharacter_texture_type = NoLanguageCharacter_texture_type.Normal): any {
    return this._textures.get(type);
}

Rule(s)

Example (solution)

// Overloads => multiple signatures without implementation:
public static Delete(motive: Mongoose.LeanDocument<Motive>): Promise<Mongoose.LeanDocument<Motive>>;
public static Delete(motive_number: string): Promise<string>;
// Implementation by means of union types:
public static Delete(m: Mongoose.LeanDocument<Motive> | string) {
    return typeof m === "string" ?
        MotiveModel.deleteOne({motive_number: m}).exec() :
        MotiveModel.deleteOne(m).exec();
}

Example (using conditional types…) Conditional_type.ts.zip 

class A {
    constructor(readonly a: number) { }
}
class B {
    constructor(private readonly _b: string) { }
}
function f(n: number): A; // Overloading (spec.) // Evite : 'const r2: A = <A>f(0);'
function f(s: string): B; // Overloading (spec.)
function f(x: number | string): A | B { // Implementation
    return typeof x === "number" ? new A(x) : new B(x);
}
// const r1: B = f(0); // Compilation error...
const r2: A = f(0); // OK

// Le type 'X' est défini comme conditionnel, il est générique paramétré par 'T' compatible à 'number' ou 'string' :
type X<T extends number | string> = T extends number ? A : B;
// La fonction 'g' est générique paramétrée par 'T'compatible à 'number' ou 'string' :
function g<T extends number | string>(x: T): X<T> {
    return typeof x === "number" ? new A(x) as X<T> : new B(x) as X<T>; // Horrible mais bon...
}
// const a_: B = f(0); // Compilation error...
const a_: A = g(0); // OK

type Y<T extends { a: unknown }> = A["a"];
const y: Y<A> = 0; // Le type de 'y' est 'number'...
// const z: Y<B> = true; // Compilation error...

Inheritance in TypeScript

Rule(s)

Example Inheritance_polymorphism.ts.zip 

abstract class Animal {…}

abstract class Mammal extends Animal {…}

abstract class Oviparous extends Animal {…}

// class Ornithorhynchus extends Mammal, Oviparous { // Not supported while it is in C++!
// }

class Giraffe extends Mammal {
    constructor(private readonly _name: string, private readonly _height: number) {
        super(); // Mandatory
    }
}

Rule(s)

Example

export default abstract class NoLanguageCharacter {
    protected abstract _hex_color(): number; // No code...
    …

Example

export default class Troublemaker extends NoLanguageCharacter { // No longer abstract...
    protected _hex_color(): number { // Cannot "become" 'private' while it may become public... 
        return …;
    }
    …

Rule(s)

Example

abstract class Friend {
    protected constructor(private readonly _nickname: string) {
    }
}

class Close_friend extends Friend {
    public constructor(/* 'private readonly' tries the creation of another '_nickname' private attribute! */ _nickname: string) {
        super(_nickname);
    }
}

Rule(s)

Example

interface Responsibility { /* ... */ }

abstract class Individual {
    abstract responsibilities?(): Set<Responsibility>; // Abstract and optional...
    abstract isResponsible(responsibility: Responsibility): boolean;
}

class Kid extends Individual {
    responsibilities: undefined; // Implementation allows 'Kid' to no longer be abstract...
    isResponsible(responsibility: Responsibility): boolean {
        return false; // 'false' in all cases: 'responsibility' is ignored...
    };
}

Polymorphism in TypeScript

Rule(s)

Example Inheritance_polymorphism.ts.zip 

class Female extends Individual {

    cloning(): Female {
        console.log("cloning in Female...\n");
        return new Female(new Date(this._birth_date).toDateString());
    }
}
…
// Later on:
let Eve: Individual = new Female("0, 0, 0");
let Eve_clone: Individual = Eve.cloning(); // 'cloning in Female...' is displayed => covariance applies in TypeScript!

Using this as a type

Example

class Individual {
    …
    that_s_me(): this { // This differs from 'Current' polymorphic type in Eiffel!
        return this; // Only 'this' object complies with 'this' *TYPE*!
    }
}
…
// Later on:
console.assert(Eve.that_s_me().constructor === Female); // 'true' is displayed!

Using this in conjunction with bind

Example

class My_class {
    private readonly _resolve_2020: Promise<number> = Promise.resolve(2020);
    …
        this._resolve_2020.then(function (this: My_class, value: number) { // Differently to JavaScript, TypeScript makes explicit the type of 'this' in the function...
            console.assert(value === 2020);
            // Use 'this' as effective instance of 'My_class'...
        }.bind(this));

Example

interface Person {
    identities: Array<string>;
    display?: Function;
}

const person: Person = {
    identities: ["Barbier", "Franck", "Bab"],
    display: function (this: Person) { // Differently to JavaScript, TypeScript makes explicit the type of 'this' in the function...
        console.assert(person === this);
        this.identities.forEach(function (element) {
            window.alert(element);
        }.bind(this)); // This guarantees 'console.assert(person === this)'
    }
};

person.display();
interface keyword

Rule(s)

Example

export enum Access { // 'export' allows external usage
    Failure,
    Success,
}

export interface Image_access {
    access: Access;
    image_URL: string;
}

export interface Open_Food_Facts_item {
    images: Array<Promise<Image_access>>;
    image_URLs: Array<string>;
    product_name: string;
}
…
// Somewhere else:
private readonly _item_data: Promise<Open_Food_Facts_item>;

Functional interface

Rule(s)

Example

interface Main { // Similar to Java "functional interface" -> "function type" in TypeScript
    (): void; // Call signature
}
…
// Somewhere else:
const main: Main = () => { (…) } // Ellipses embody concretization 

Note(s)

Interface concretization

Rule(s)

Example

interface Currency { // Example of USA dollar:
    common_symbol: string; // $
    common_name: string; // Dollar
    description?: string;
    iso_code: number; // 840
    iso_symbol: string; // USD
    substitute?: Currency; // 'undefined'
    substitution_date: number | null;
}

class Currencies {
    public static readonly Currencies: Array<Currency> = [
        {
            common_symbol: "$",
            common_name: "Dollar",
            description: "First leading currency in the world...",
            iso_code: 840,
            iso_symbol: "USD",
            substitution_date: null
        },
        {
            common_symbol: "€",
            common_name: "Euro",
            description: "Second leading currency in the world...",
            iso_code: 978,
            iso_symbol: "EUR",
            substitution_date: null
        }
    ];
}

interface Iso_code_checking { // Interface as contract...
    already_exists: (iso_code: number) => Promise<boolean>; // Attribute as lambda expression...
}

class Iso_code_checking_service implements Iso_code_checking { // Class as service...
    already_exists(iso_code: number): Promise<boolean> { // Concretization...
        const exists = Currencies.Currencies.find(currency => currency.iso_code === iso_code);
        // Check on the Web as well....
        return Promise.resolve(exists !== undefined ? true : false);
    }
}

Rule(s)

Example

interface AsyncValidator {
    validate(ac: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
}
…
class My_asynchronous_validator implements AsyncValidator {
    validate(ac: AbstractControl): Promise<ValidationErrors | null> { /* Concrete code here... */ }
}

Generic interface

Rule(s)

Example Palindrome.ts.zip 

interface Pair<T> {
    readonly _first: T; // Cannot be 'private'...
    readonly _second: T;
    description?: string;
}

class Palindrome_authorized_interval implements Pair<string> {
    // Caution: attributes from interface *MUST* be repeated here...
    // This is done through constructor:
    constructor(readonly _first: string, readonly _second: string) {
    }

    /* 'private' -> error because 'public' in 'Pair<T>' */
    description: string; // May be omitted because optional in 'Pair<T>'

    set_description(/* 'readonly' -> constructor only! */ description: string): void { // 'public' by default
        this.description = description;
    }
}

Interface inheritance

Rule(s)

Example

// 'CustomEvent' comes from the 'DOM' library (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent):
interface My_event_type extends CustomEvent {
    additional_attribute?: symbol;
}

Rule(s)

Example Palindrome.ts.zip 

enum Comparable_ {LessThan, Equal, GreaterThan}

interface Comparable<T> { // Similar to Java "functional interface"
    compareTo(t: T): Comparable_; // Method signature
}

interface Pair<T> extends Comparable<Pair<T>> {
    readonly _first: T; // Cannot be 'private'...
    readonly _second: T;
    description?: string;
}

Example (abstract nature of Palindrome_authorized_interval class is removed…) Palindrome.ts.zip 

class Palindrome_authorized_interval implements Pair<string> {
    …

    compareTo(t: Pair<string>): Comparable_ { // 'public' by default
        if (this._first.localeCompare(t._first) === -1 &&
            this._second.localeCompare(t._second) === -1) return Comparable_.LessThan;
        if (this._first.localeCompare(t._first) === 0 &&
            this._second.localeCompare(t._second) === 0) return Comparable_.Equal;
        if (this._first.localeCompare(t._first) === 1 &&
            this._second.localeCompare(t._second) === 1) return Comparable_.GreaterThan;
    }

    …
}

Example (abstract nature of Complex class is kept…)

abstract class Complex implements Pair<number> {
    constructor(readonly _first: number, readonly _second: number) {
    }

    abstract compareTo(t: Pair<number>): Comparable_;

    …
}

Rule(s)

Example

interface Man {
    children?: Array<Individual>;
}

interface Father extends Man {
    children: Array<Individual>; // Compilation time...
}

function isFather(man: Man): man is Father {
    return 'children' in man && man.children!.length > 0; // Execution time...
}

function elder(man: Man): Individual {
    if (isFather(man)) 
        return man.children[0]; // No compilation error!
}