Static Type Checking of Classes

Classes were introduced to JavaScript ecosystem by ES5.

Instance Members

Instance Properties

A class in JavaScript can have instance properties. They can be declared with or without initializers.

After the class initialization, the instance properties become properties of the object created through initialization.

class Foo {
  bar = 42
  baz
}

const qux = new Foo()
qux.bar // => 42
qux.baz // => undefined

TypeScript allows for restricting types of instance properties.

class Foo {
  bar: number = 42
  baz: string
}

const qux = new Foo()
qux.bar = 23 // OK
qux.bar = '23' // Type 'string' is not assignable to type 'number'.
qux.baz = '42' // OK
qux.baz = 42 // Type 'number' is not assignable to type 'string'.

If no type is annotated and no initializer is present at an instance property it has the implicit any type. If an initializer is present TypeScript will attempt type inference.

It is possible to enforce property initialization in constructor through setting the flag strictPropertyInitialization in tsconfig.json to true.

Instance Method Signatures

When an instance property's value is a function then the property is called an instance method.

Just like with non-method functions TypeScript allows for annotating instance method parameter types and return types.

class Student {
  firstName = 'Joe'

  changeFirstName(newFirstName: string): string {
    this.firstName = newFirstName

    return firstName
  }
}

const student = new Student()
student.changeFirstName('Other Joe') // => Student {firstName: 'Other Joe'}
student.changeFirstName(42) // TS: Argument of type 'number' is not assignable to parameter of type 'string'.

This in Instance Methods

Within a body of an instance method, the this keyword might but does not have to reference an instance of the class within the body of which the method was defined.

Generally, the value referenced by this in a JavaScript non-arrow function does not depend on how the function was defined but on how it is called. For extended reference see soundof.it JavaScript Tutorial - This.

class Captain {
  name = 'Hook'
  ship = 'Jolly Roger'

  logName() {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logFavoriteCaptain: captain.logName
}

student.logFavoriteCaptain() // Mark

The probable intention of the above code was to log the student favorite captain's name. Instead, the name of the student was logged. TypeScript can catch such unintended operations through type annotation of this in instance methods.

class Captain {
  name = 'Hook'
  ship = 'Jolly Roger'

  logName(this: Captain) {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logFavoriteCaptain: captain.logName
}

student.logFavoriteCaptain()
// The 'this' context of type '{ name: string; logFavoriteCaptain: (this: Captain) => void; }' is not assignable to method's 'this' of type 'Captain'.
// Type '{ name: string; logFavoriteCaptain: (this: Captain) => void; }' is missing the following properties from type 'Captain': ship, logName.

However, it needs to be remembered that TypeScript checks the compatibility of types structurally. Therefore, TypeScript will not find any problem with the following example.

class Captain {
  name = 'Hook'

  logName(this: Captain) {
    console.log(this.name)
  }
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logName: captain.logName
}

student.logName() // Mark

The initial parameter named this in an instance method (or any other function) references the type of JavaScript this and is erased during compilation.

A thing to remember is that an arrow instance method behaves differently when it comes to this to the non-arrow instance method.

class Captain {
  name = 'Hook'

  logName = () => console.log(this.name)
}

const captain = new Captain()
const student = {
  name: 'Mark',
  logName: captain.logName
}

student.logName() // Hook

In other words, the value referenced by this within an arrow function does depend on how it was defined and not on how it is called.

Instance Property Accessibility

When it comes to accessibility an ES5 class can have two kinds of instance properties:

  • public - defined or declared without any prefix, and

  • private - defined or declared with # prefix.

At runtime, public properties are freely accessible. On the other hand, private properties are only accessible within the class in which they are declared.

class Foo {
  bar = 42
  #baz = 23
}

const qux = new Foo()
qux.bar // => 42
qux.baz // => undefined
qux.#baz // Uncaught SyntaxError: Private field '#baz' must be declared in an enclosing class

class Baz extends Foo {
  logBaz() {
    console.log(this.#baz) // Property '#baz' is not accessible outside class 'Foo' because it has a private identifier.
  }
}

Private properties defined or declared with # are ES5 - not TypeScript - specific feature.

TypeScript features its pre-compilation property modifiers when it comes to accessibility:

  • public

  • soft protected, and

  • soft private.

Public properties are denoted with no modifiers or by the keyword public and accessible without limitations.

Protected properties are denoted by the modifier protected and are accessible only within the class within which they were declared and within its subclasses.

Private properties are denoted by the modifier private and when using the dot notation are accessible only within the class within which they were declared.

The protected and private properties in TypeScript are called soft protected and soft private because it is still possible to access them using the bracket notation.

class Captain {
  public name = 'Hook'
  protected ship = 'Jolly Roger'
  private nemesis = 'Peter Pan'
}

const captain = new Captain()
console.log(captain.name) // 'Hook'
console.log(captain.ship) // Property 'ship' is protected and only accessible within class 'Captain' and its subclasses.
console.log(captain.nemesis) // Property 'nemesis' is private and only accessible within class 'Captain'.
console.log(captain['ship']) // 'Jolly Roger'
console.log(captain['nemesis']) // 'Peter Pan'

class starshipCaptain extends Captain {
  name = 'Jean Luc'
  ship = 'Enterprise'
  nemesis = 'Q'
}
// Class 'starshipCaptain' incorrectly extends base class 'Captain'.
// Property 'nemesis' is private in type 'Captain' but not in type 'starshipCaptain'.

In the above example the class starshipCaptain attempts to make the ship and nemesis properties public. It succeeds with regards to the ship property (as it is accessible to it) but fails with regards to the nemesis property (as it is inaccessible to it).

One other thing to remember about TypeScript private properties is that they are cross-instance accessible (similarly to Java, C# and PHP) as opposed to private properties in Ruby.

Constructors

Class constructors in JavaScript are functions that are being called at class instance initialization. During the call the this keyword in the constructor denotes the instance being created. It allows for initialization of not yet initialized properties and/or other operations.

A constructor always implicitly returns the instance and using the keyword return within its body is not allowed.

// ES5
class Student {
  firstName
  lastName

  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

const student = new Student('Ciri', 'Riannon')
student // => Student {firstName: 'Ciri', lastName: 'Riannon'}
student.firstName // => 'Ciri'
student.lastName // => 'Riannon'

Just like with regular functions TypeScript allows for restricting types of constructor parameters.

// TypeScript
class Student {
  firstName: string
  lastName: string

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

const student = new Student(42, 42) // TS: Argument of type 'number' is not assignable to parameter of type 'string'.

Static Members

A class in JavaScript can have static members i.e. members that are not associated with and specific to instances of the class but with the class object itself.

class Captain {
  name: string
  static captainNames: string[] = []

  constructor(name: string) {
    this.name = name
    Captain.captainNames.push(name)
  }
}

const hook = new Captain('Hook')
const blackbeard = new Captain('Blackbeard')
const janeway = new Captain('Janeway')

console.log(Captain.captainNames) // (3) ['Hook', 'Blackbeard', 'Janeway']

Static class members are inherited.

Static class members can be made private at runtime using the # modifier. Further, TypeScript can alter their pre-runtime accessibility with public, protected and private modifiers.

Checking Implementation

TypeScript allows for static checking of congruency of class instance members minimal implementation with a specific contract outlined by an interface or a type alias.

To check whether a given class correctly implements an interface or a type alias the keyword implements is used appended with the interface name or type alias.

interface Captainable {
  name: string
  ship: string
}

class Captain implements Captainable {
  name: string
  ship: string
  nemesis: string // Additional property not present in the interface which is OK.
} // OK

class Student implements Captainable {
  firstName: string
  lastName: string
  major: string
}
// TS: Class 'Student' incorrectly implements interface 'Captainable'.
// TS: Type 'Student' is missing the following properties from type 'Captainable': name, ship.

In the above example the class Captain meets the interface Captainable contract as it has all the required properties with the specified types whereas the class Student does not.

A congruency with the implementation of more than one interface or type alias simultaneously is possible to be checked.

interface Nameable {
  name: string
}

interface Shipable {
  ship: string
}

class Captain implements Nameable, Shipable {
  name: string
  ship: string
} // OK

The implementation check with the implements keyword in no way supersedes or mimics the class extension using the keyword extends!