Books / TypeScript for Java Developers by CodeAhoy / Chapter 2
OOP in TypeScript
Classes
Class is the basic unit containing all data and behaviors inside it. It is a template from which we create objects that we use at runtime.
Java is an Object Oriented Programming (OOP) and classes are everything in it. You can’t write even the most basic Java program without using classes. JavaScript, as we learned in the last chapter, is a functional programming language.
TypeScript supports Classes that are very similar to Java classes and it supports many common patterns such as implementing interfaces, inheritance, and static methods. However, unlike Java, classes are optional in TypeScript.
TypeScript - Classes are Optional
To illustrate this point, let’s write a simple Hello World program in both Java and TypeScript.
First in Java.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Now in TypeScript:
console.log("Hello, World!");
Notice that in Java, we had a write extra code just to print “Hello, World!”. This is because all Java code must be contained within a class. The main(...)
method, which is the entry point for a Java program, must also be defined within a class. Therefore, in order to output “Hello, World!” in Java, we need to define a class and a main()
method within that class.
In contrast, we don’t need classes in TypeScript. Hence, we can simply use the console.log()
function to output “Hello, World!” in a TypeScript program, without the need to define a class.
However, that is not to indicate classes are any less important in TypeScript. Many problems are modeled best using the OOP paradigm and best represented using classes and objects in both Java and TypeScript.
Types
In Java, all objects have a type. The type is a class that we can easily determine at run-time or compile time because Java objects are related to their type when we declare them. There’s one-to-one correspondence between runtime types and objects and their compile-time declarations. A method in Java that excepts a particular type e.g. someMethod(MyClass o)
will only accepts argument that are of type MyClass
(unless there’s a inheritance relationship or common interface.)
In TypeScript, we think of a type as a set of values that share something in common. A value can belong to many sets at the same time.
What is Duck Typing?
For example, in Java, we cannot pass around a value that is either a String
or Integer
, because there isn’t a single type that represents this sort of value (unless you create one using hierarchy but it’s not natural.) In TypeScript, this is natural since every type is just a set. How do you describe a value that’s either a string or a number? Simple: string | number
.
If you’d like to learn more about how types are represented in TypeScript compared to Java, this resource is highly recommend.
Differences Between TypeScript and Java Classes
There are a few key conceptual and syntactical differences between classes in Java and TypeScript:
-
TypeScript has a more flexible class model than Java. In TypeScript, you can use the
class
,abstract class
,interface
, andtype
keywords to define different types of classes and class-like constructs. In Java, there is only one type of class, and you can use theabstract
andfinal
keywords to modify the behavior of a class. -
TypeScript has a more advanced type system than Java, which allows you to specify the types of class properties and method parameters more precisely. In Java, you can specify the type of a class property or method parameter using type annotations, but the type system is not as powerful as it is in TypeScript.
-
TypeScript supports access modifiers like
public
,private
, andprotected
, which can be used to control the visibility and accessibility of class members. In Java, we can use thepublic
,private
,protected
, anddefault
to specify the access level of a class member. -
TypeScript has a
readonly
modifier that can be used to mark class properties as read-only. Whereas in Java, you can use thefinal
keyword to mark a class property as read-only, but there is no equivalent to thereadonly
modifier. -
In TypeScript, an object can belong to multiple classes at the same time. This is not possible in Java.
-
In TypeScript, you can use Mixins to add functionality to classes without using inheritance of interfaces.
What are Mixins in TypeScript?
Constructors
In both TypeScript and Java, you can use constructors to initialize an object when it is created.
In Java, you can define a constructor for a class like this:
public class MyClass {
private String name;
public MyClass(String name) {
this.name = name;
}
}
In TypeScript, you can define the same constructor and class like this:
class MyClass {
constructor(public name: string) {
// constructor implementation goes here
}
}
Here we can see that the constructor is is a special function that is called when an object of the MyClass
class is created. The name
parameter is a public
class property (variable) that can be accessed from outside the class.
Constructor definition is optional in both languages because the language generates a default constructor for you.
It is also possible to define multiple constructors in both TypeScript and Java. Overloading allows you to define multiple constructors for a class where each constructor has different arguments. This allows you to create objects and initialize their properties differently based on the constructor used to create that object.
super()
calls
Just like Java, if your class extends your a base class, the super
keyword refers to parent class.
What is super keyword in Java?
Here’s an example:
// Parent class
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); //call the constructor of parent class i.e. Animal
}
move(distanceInMeters) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let s = new Snake("Kaa the Python");
s.move(5); // Output: "Kaa the Python moved 5m."
Access Modifiers: public
, private
, protected
methods
In both TypeScript and Java, you can use access modifiers like public
, private
, and protected
to control the visibility and accessibility of your class members (fields, properties, and methods). Unlike Java, in TypeScript, if you don’t specify an access modifier, the member will be public by default.
- public:
public
members are visible and accessible from anywhere, both within and outside the class. This is the default visibility if no access modifier is specified. - private: Members marked as
private
are only visible within the class where they are declared. They cannot be accessed from outside the class. - protected: Members marked as
protected
are visible and accessible within the class where they are declared, as well as in any subclasses. They cannot be accessed from outside the class or any of the subclasses. A constructor can also be markedprotected
. This means that the class cannot be instantiated outside of its containing class, but can be extended or inherited from.
Can we override a protected method in Java?
class Animal {
protected name: string;
public age: number;
private location: string;
constructor(name: string, age: number, location: string) {
this.name = name;
this.age = age;
this.location = location;
}
protected move(distanceInMeters: number) {
console.log(`${this.name} who's ${this.age} years old moved ${distanceInMeters}m in ${this.location}`);
}
}
class Snake extends Animal {
constructor(name: string, age: number, location: string) {
super(name, age, location);
}
public increaseAge(age: number) {
super.age = age // age is protected so we can access in the subclass
}
public crawl() {
this.move(5);
}
}
let kaa = new Snake("Kaa the Python", 3, "Africa");
kaa.increaseAge(10)
console.log(kaa.age); // Output: 10 (age is public can be assessed outside the class)
console.log(kaa.location); // Error: location is private and can only be accessed within class 'Animal'
console.log(kaa.name); // Error: name is protected and can only be accessed within class 'Animal' and its subclasses.
kaa.crawl(); // Output: "Kaa the Python who's 10 years old moved 5m in Africa"
readonly Modifier
You can make properties readonly by using the readonly
keyword. Readonly properties must be initialized at the time of declaration or in the constructor.
class Octopus {
readonly name: string;
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 Cannot assign to 'name' because it is a read-only property.
Importing Classes
In TypeScript, we use the import
keyword to import classes, functions, and other values from another module. For example:
import { MyClass } from './my-module';
This will import the MyClass
class from the my-module
module.
You can also import the entire module and access the exports using the module’s name:
import * as myModule from './my-module';
let myClass = new myModule.MyClass();
In Java, you can use the import
keyword to import classes from another package. For example:
import com.example.MyClass;
This will import the MyClass
class from the com.example
package. We can also use the *
wildcard to import all of the classes in a package:
import com.example.*;
One key difference between TypeScript and Java is that TypeScript uses a module system, whereas Java uses a package system. In TypeScript, modules are used to organize and reuse code, whereas in Java, packages are used to group related classes and define a namespace for the classes.
This is not to be confused with Java Modules introduced in Java 9 which is a way to package related Java packages and associated resources with a descriptor file as a modular JAR file.
Module vs Packages
In TypeScript, a module is a way to organize and reuse code. A module is a collection of related variables, functions, and classes that are encapsulated together and can be imported and used by other parts of your code.
Modules in TypeScript are similar to the concept of libraries or packages in other programming languages. They allow you to divide your code into smaller, more manageable pieces and reuse those pieces in multiple places.
Modules in TypeScript are defined using the export
keyword. For example, you can define a class in a module like this:
export class MyClass {
// class implementation goes here
}
You can then import that class into another module or file using the import
keyword:
import { MyClass } from './my-module';
let instance = new MyClass();
Here are key differences between Modules and Packages:
-
Java packages are used to group related classes and define a namespace for those classes. In TypeScript, modules are used to organize and reuse code, and do not necessarily define a namespace.
-
Both packages and modules help resolve naming conflicts, but Java packages are hierarchical unlike TypeScript
package com.example;
public class MyClass {
}
package com.example.subpackage; // Subpackage
public class MyClass {
}
-
Packages are mandatory in Java and all Java programs are organized as sets of packages. Modules are optional in TypeScript.
-
In Java, packages are defined using the
package
keyword are all classes in the same package can usepublic
andprotected
methods without importing. In TypeScript, modules are defined using theexport
andimport
keywords, and must be explicitly imported to be used anywhere.
Inheritance
In both TypeScript and Java, inheritance allows you to create a subclass that inherits properties and methods from the superclass and can also have its own properties and methods. For example in Java, we can define the Animal
class is the superclass, and the Dog
class is the subclass. The Dog
class extends the Animal
class and inherits its move()
method. It also has its own bark()
method.
public class Animal {
// Properties and methods go here
public void move() {
System.out.println("The animal is moving");
}
}
public class Dog extends Animal {
// Properties and methods go here
public void bark() {
System.out.println("Woof woof!");
}
}
Dog dog = new Dog();
dog.move(); // Output: "The animal is moving"
dog.bark(); // Output: "Woof woof!"
Here’s the same class hierarchy and inheritance in TypeScript:
class Animal {
// Properties and methods go here
move(): void {
console.log("The animal is moving");
}
}
class Dog extends Animal {
// Properties and methods go here
bark(): void {
console.log("Woof woof!");
}
}
let dog = new Dog();
dog.move(); // Output: "The animal is moving"
dog.bark(); // Output: "Woof woof!"
Note: TypeScript has support for mixins, which allow you to combine the properties and methods of multiple classes into a single class, while Java does not have this feature.
Interfaces
An interface defines a contract that specifies the behavior that a class or object must have, but it does not provide any implementation details. It defines a contract that classes and objects must adhere to. Both TypeScript and Java have support for interfaces, but they have some differences in how they are implemented and used.
Here’s how interfaces are defined and used in Java:
public interface Shape {
// Properties and methods go here
double area();
}
public class Square implements Shape {
// Properties and methods go here
private double sideLength;
public double area() {
return sideLength * sideLength;
}
}
Shape square = new Square();
square.sideLength = 10;
System.out.println(square.area()); // Output: 100
In this example, the Shape
interface defines an area()
method that must be implemented by any class that implements the interface. The Square
class implements the Shape
interface and provides an implementation for the area()
method.
In TypeScript, an interface is defined using the interface
keyword, and it can contain properties and methods that a class or object must implement. An interface can also have optional properties and methods, which are marked with a ?
at the end of the property or method name.
interface Shape {
// Properties and methods go here
area(): number;
}
class Square implements Shape {
// Properties and methods go here
sideLength: number;
area(): number {
return this.sideLength * this.sideLength;
}
}
let square = new Square();
square.sideLength = 10;
console.log(square.area()); // Output: 100