TypeScript Object-Oriented Concepts in a Nutshell

All the major aspects of TypeScript objects in one place so you don’t have to google around for them

Sean Maxwell
Level Up Coding

--

(TypeScript === JavaScript with Superpowers) => true

If you come from a traditional object-oriented (OO) background like me and are daunted by the quirks of JavaScript — not to fear — because TypeScript has got you covered. This tutorial is going to skim through all the major points of TypeScript objects while giving examples along way and showing you when they come in handy. The features of TypeScript objects overlap heavily with traditional OO languages like Java and C#, so some of these you may already be familiar with these points if you’re a seasoned programmer. If not, this will be a great overview of OO style programming.

TypeScript Objects vs JavaScript Objects

TypeScript objects are just syntax-sugar for JavaScript function-objects. There’s a lot of repetitive code when it comes to using function-objects as classes in JavaScript, that’s why class was implemented for ES6. TypeScript takes ES6 classes to a higher plane of reality by adding — not only types — but also object features such as public, private, abstract, etc. If you’re interested in learning more about quirks of JavaScript function-objects (which I highly recommend doing) please checkout my Medium article here.

Object Overview

Objects in TypeScript (like in all of OO programming) are useful because they let us model our program off of real world scenarios. Objects are instances (a particular variable with that object as its value type) of classes. Think of classes as just a group methods and variables. Let’s say we wanted to have a program called PetStore that only sells cats and dogs. A dog could have attributes like age and breed but also methods like getRelativeAge, if we want the age in dog years. If we’re talking about a particular dog object such as Spot or Bingo, who have values set for their attributes, then those objects are instances of the Dog class.

Classes are implemented using the class keyword, just like in ES6. To create an object we call the class with the new keyword, which triggers the constructor and returns an instance-object just like in regular JavaScript. Since objects in JavaScript are technically just a set of key/value pairs, I like to use the term instance-object to refer to objects returned with the new keyword.

Spot is an instance-object of Dog.

class Dog
{
age: number
breed: string
constructor(age: number, breed: string)
{
this.age = age
this.breed = string
}
getRelativeAge(): number
{
return this.age * 7
}
}
let Spot = new Dog(2, 'Labrador')

Equivalent using function-objects in ES5.

function Dog(age, breed)
{
this.age = age
this.breed = breed
}
Dog.prototype.getRelativeAge = function() {
return this.age * 7
}
var Spot = new Dog(2, 'Labrador')

Inheritance

Now that you know how to make objects and can see how they work under the hood in JavaScript, let’s start learning about TypeScript inheritance. In our PetStore program, we’re selling dogs and cats but there could be different breeds of dogs and cats right? Also dogs and cats might share some of the same attributes like age and weight. Super classes, (aka parent classes) allow for related objects to be grouped together so that they can inherit similar attributes. To inherit from a parent classes we use the extends keyword. When you extend a class, all attributes and methods with be passed down. Instead of creating age and weight each time we add a new animal to our pet store, we can now just expand upon a parent Animal class. Multi-level inheritance is also possible by extending child classes.

The super keyword serves two roles in inheritance. First it acts as a function and is used in the parent class’s constructor. It has to be called before this in the child object constructor. The other is that it allows us to access methods (but NOT attributes) of the parent object.

Animal is the parent class of Dog:

class Animal
{
age: number
breed: string
constructor(age: number, breed: string)
{
this.age = age
this.breed = breed
}
makeSound_(sound: string): void
{
console.log(sound)
console.log(sound)
console.log(sound)
}
}

Basic inheritance using super:

class Dog extends Animal
{
playsFetch: boolean
constructor(age: number, breed: string, playsFetch: boolean)
{
super(age, breed) // call parent constructor
this.playsFetch = playsFetch
}
makeSound(): void
{
super.makeSound_('woof woof')
}
getAgeInHumanYears(): number
{
return this.age * 7 // super.age will throw error
}
}
class Cat extends Animal
{
constructor(age: number, breed: string)
{
super(age, breed)
}
makeSound(): void
{
super.makeSound_('meow meow')
}
}

JavaScript ES6 inheritance works the same way, but in TypeScript there is an additional access-control feature when working with parent classes. ES6 also does not allow for class level variables to be defined outside of methods.

Access-Control

Access-control refers to where we can use a class’s attributes and methods. If you’ve ever skimmed through an OO language’s code, you might have noticed keywords like public, private, and protected. Let’s go over what each one of these is for and why they are useful.

Suppose our PetStore program has a class named PetStore. If this class wants to call methods on our Dog objects, then those methods will need to be marked public. When a method or variable is public that means it can be accessed by another part our program. When we don’t have any modifier on a variable or method, it’s the same as marking it public.

class Dog
{
public name: string // leaving out 'public' would work too
}
class PetStore
{
dogs: Array<Dog>
printAllDogNames(): void
{
this.dogs.forEach(dog => {
console.log(dog.name)
})
}
}

Allowing other coders to directly access attributes of objects generally is not a good idea though. It’s better to use getters and setters to access/modify class properties so we can pass some logic when setting a value and prevent errors. For example, a dog’s name shouldn’t be falsey and it should be under a certain length. A realistic dog name would never be more than 10–20 characters. To make an class variable/method only accessible within that class we should mark it private. TypeScript classes have built in get and set modifiers which will trigger our getters and setters whenever a property is trying to be accessed.

class Dog
{
private _name: string // beginning underscore is convention
get name(): string
{
return this._name
}
set name(name: string): void
{
if(!name || name.length > 20) {
throw new Error('Name invalid')
}
else {
this._name = name
}
}
}
class PetStore
{
private _dogs: Array<Dog> // we changed this to private too
constructor()
{
this._dogs = [new Dog()]
this._dogs[0].name = 'Fido' // will call 'set'
}
printAllDogNames(): void
{
this._dogs.forEach(dog => {
console.log(dog.name) // will call 'get'
})
}
}

Lastly, let’s look at the protected keyword. Protected means that a variable/method can only be accessed in child classes of the parent. Remember the makeSound method of the Animal parent class? We shouldn’t be able to access that method externally on any class that inherits from animal or animal itself because not animals make sounds. I like to post-pend protected attributes with an ending underscore, although it’s not convention.

class Animal
{
protected makeSound_(sound: string): void
{
console.log(sound)
console.log(sound)
console.log(sound)
}
}
class Dog extends Animal
{
makeSound(): void
{
super.makeSound_('woof woof')
}
}
class PetStore
{
makeSomeSounds(): void
{
let dog = new Dog()
dog.makeSound() // => 'woof woof' 'woof woof' 'woof woof'
let animal = new Animal()
animal.makeSound_() // => NOT ALLOWED
}
}

Other Modifiers

There are 2 other modifiers that important to mention when talking about TypeScript classes: static and readonly. If we want to access a property on a class without having to go through the trouble of returning an instance-object (calling the object with new), then we can mark it as static and it will be set on the class (function-object) itself. This is useful for methods and class variables which are not dependent on any dynamic property. For example, a dog will always be the same species.

class Dog
{
static species = 'Canis Familaris'
age = 10
}
class PetStore
{
printSpecies(): void
{
console.log(Dog.species) // => 'Canis Familaris'
console.log(Dog.age) // => undefined
}
}

The readonly keyword is pretty self-explanatory. It’s used for class level variables and means that the value cannot be reassigned. Values that are initialized when the class is created and you know are never going to change should be readonly. Our Dog class’s species property is a good example. No matter what attributes we assign to dog it will always be the same species.

class Dog
{
static readonly species = 'Canis Familaris'
}
class PetStore
{
printSpecies(): void
{
console.log(Dog.species) // => 'Canis Familaris'
Dog.species = 'Terdus Maximus' // => NOT ALLOWED
}
}

Interfaces

Whenever we want to say that an object being passed has a specific set of attributes, we can use an interface. Interfaces are nifty little tools that can be used for several situations.

The most immediate thing that comes to mind is testing. Suppose we have a method in our Dog class that does an I/O call, and we wanted to unit test a method in our PetStore class which called that method. We don’t want to fire an I/O call every time a unit-test is run but we still need an object which evaluates as the Dog type. Let’s create an iDog interface which specifies a method for both the regular and mock class that we create for our unit-test.

interface iDog
{
getPedigree: Function
}
class Dog implements iDog
{
getPedigree(): Promise<Pedigree>
{
return someThirdPartyIoCall('...')
}
}
class MockDog implements iDog
{
getPedigree(): Promise<Pedigree>
{
return new DummyPedigreeObject()
}
}
async function methodToBeTested(dog: iDog): Promise<void>
{
try {
let pedigree = await dog.getPedigree()
// do assertions here
}
catch(err) {
console.log(err)
}
}

// Real World
methodToBeTested(new Dog())

// During Testing
methodToBeTested(new MockDog())

This is only one small example of using interfaces and there are plenty of more uses. I recommend checking the TypeScript Docs here for more information.

Abstract Classes and Methods

Think of abstract classes as kind of a cross between regular parent classes and interfaces. Abstract classes define attributes for other classes like interfaces do, but some of their methods may contain implementation unlike interfaces. A method without implementation must be marked abstract, and so must its containing object. Abstract classes may not be instantiated (can’t use new) and useful for when you know you’ll never need the parent class directly.

For both cats and dogs we have the age property and want to know each one’s age in human years. The method for defining this though will be different depending on the animal. Let’s use an abstract class.

abstract class Animal
{
protected age_: number
abstract getRelativeAge(): number;
}
class Dog extends Animal
{
getRelativeAge(): number
{
return this.age_ * 7
}
}
class Cat extends Animal
{
getRelativeAge(): number
{
return this.age_ * 6
}
}

Note: This is not meant to be an accurate representation of how to properly calculate a cat’s and a dog’s age.

Conclusion

There is way more to TypeScript classes than what was covered in this tutorial, but hopefully this quick easy rundown helped put things into perspective a little better. Whether or not you or others intend to use TypeScript extensively, the object-oriented concepts covered here overlap so much with other OO languages it’s still a very useful read. Typescript is also nice for learning OO because such a less formal language than Java, C#, or C++.

--

--