Skip to main content

Classes_TypeScript Notes 4

Free2019-01-27#TypeScript#TypeScript Class#TS类继承#TS访问控制#TypeScript成员修饰符

Not only are member modifiers, abstract classes and other features fully enabled, but also without adding runtime overhead

I. Class Members

Class definitions in TypeScript are consistent with [ES6 Class specification](/articles/class-es6 笔记 10/), supporting static properties, instance properties, accessors, etc.:

class Grid {
    static origin = {x: 0, y: 0};
}

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}

But need to note 2 points:

  • ES3 doesn't support getter/setter, therefore requires compilation configuration to ES5+

  • Properties with only getter without setter will be automatically inferred as readonly (one of member modifiers)

II. Member Modifiers

Access Control Modifiers

Supports 3 access control modifiers:

  • public: Class member properties/methods are public by default, no access restrictions

  • private: Cannot access its members outside the class declaration (e.g., cannot access private members via this.xxx)

  • protected: Similar to private, but protected members can also be accessed in derived classes

For example:

class Animal {
    // Private member property
    private name: string;
    // Protected member property
    protected ancestor: string;
    // If not written, defaults to public
    constructor(theName: string) { this.name = theName; }
    // Public member method
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

Note, these access controls are only compile-time restrictions, no strong checks at runtime. Consistent with TypeScript's design principle:

Don't add runtime overhead to compiled output

Additionally, class member accessibility is also part of type checking, private/protected modifiers will break duck typing, for example:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
class Person {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
// Correct: duck typing (if it looks like it, it is)
let person: Animal = new Person('Sam');

class Plant {
    // Private member name
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
// Error: name property accessibility doesn't match (even if looks like, accessibility is different)
// Property 'name' is private in type 'Plant' but not in type 'Person'.
let invalidPlant: Plant = new Person('Stone');

P.S. Specially, protected constructor indicates the class doesn't allow direct instantiation, but supports inheritance

readonly Modifier

Can declare properties as read-only via readonly modifier, can only assign at declaration or in constructor, for example:

class Octopus {
    readonly name: string;
    public readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

P.S. Of course, readonly doesn't conflict with access control modifiers, can act on same property

Parameter Properties

For properties initialized in constructor:

class Octopus {
    readonly name: string;
    constructor (theName: string) {
        this.name = theName;
    }
}

There's a shorthand way:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

Where name is called parameter property, declared by adding private/protected/public/readonly modifier before constructor parameter name

III. Inheritance

class A extends B {
    //...
}

Similar to Babel transformation (for clarity, only excerpt key parts):

function _inherits(subClass, superClass) {
  // Inherit parent class prototype properties
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  // Inherit parent class static properties
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
}

// Inherit parent class instance properties in subclass constructor
function A() {
    // Add parent class instance properties to subclass instance this via parent class constructor
    return A.__proto__ || Object.getPrototypeOf(A)).apply(this, arguments)
}

Class inheritance in TypeScript will also be compiled and replaced with prototype-based inheritance, as follows:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };

    return function (d, b) {
        // Inherit parent class static properties
        extendStatics(d, b);
        function __() { this.constructor = d; }
        // Inherit parent class prototype properties, and instance properties
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

// __extends(A, B);

Both are similar, from implementation perspective, TypeScript compiled output is more robust, because its goal is:

Run in any host environment supporting ES3+

P.S. Interesting is static property inheritance, see [I. How to Inherit Static Properties?](/articles/class 继承-es6 笔记 12/#articleHeader2) for details

IV. Abstract Classes

TypeScript also has abstract class concept:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

Abstract classes can have concrete methods with implementation (like move), can also have abstract methods with only declaration without implementation (like makeSound), but requires subclasses must implement these methods:

class Cat extends Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

Another similar concept is interface, difference between the two is interfaces can only define "abstract methods" (no abstract keyword, more precisely method signatures), for example:

interface Animal {
  // Compare abstract makeSound(): void;
  makeSound(): void;
}

// Compare class Cat extends Animal
class Cat implements Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

V. Classes and Types

Declaring a class also declares the type of instances of that class, for example:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");

Where instance greeter is of type Greeter, that is, Class declarations have type meaning:

  • Type of class instances: Greeter

  • Type of class itself: typeof Greeter

Actually, type of class itself constrains static properties, instance properties, constructor, prototype methods and other characteristics, for example:

class GreeterDuck {
    // Class itself type constraints
    static standardGreeting = 'Hi';
    greeting: string;
    constructor(message: string) { }
    greet() { return 'there'; }
    // Allows more not less (duck typing)
    sayHello() { /*...*/ }
}

let greeterType: typeof Greeter = GreeterDuck;

Further:

// Extend Interface from Class type
interface HelloGreeter extends Greeter {
    sayHello(): void;
}

let hGreeter: HelloGreeter = new GreeterDuck('world');

Yes, because classes have type meaning, so interfaces can extend from classes

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment