In this blog post, we examine how class definitions work in TypeScript:
- First, we take a quick look at the features of class definitions in plain JavaScript.
- Then we explore what additions TypeScript brings to the table.
Table of contents:
Cheat sheet: classes in plain JavaScript #
This section is a cheat sheet for class definitions in plain JavaScript.
TypeScript 2.8's conditional types can be used to create compile-time inference assertions, which can be used to write tests that verify the behavior of TypeScript's inference on your API. This is a very powerful tool for improving the usability of your API. To demonstrate, let's imagine that we are building a 'pluck' function: While this may. Typescript Abstract Static Property TypeScript is the one of the tools people want to learn most, according to a Stack Overflow Survey of 90,000 developers. TypeScript has exploded in popularity, community size, and adoption over the past few years.
Basic members of classes #
Modifier: static
#
Modifier: #
(private) #
Warning:
Modifiers for accessors: get
(getter) and set
(setter) #
There are two kinds of accessors: getters and setters.
Modifier for methods: *
(generator) #
Modifier for methods: async
#
Computed class member names #
Comments:
- The main use case for this feature is symbols such as
Symbol.iterator
. But any expression can be used inside the square brackets. - We can compute the names of fields, methods, and accessors.
- We cannot compute the names of private members (which are always fixed).
Combinations of modifiers #
Fields:
Methods:
level | accessor | async | generator | visibility |
---|---|---|---|---|
(prototype) | ||||
(prototype) | get | |||
(prototype) | set | |||
(prototype) | async | |||
(prototype) | * | |||
(prototype) | async | * | ||
(prototype-associated) | # | |||
(prototype-associated) | get | # | ||
(prototype-associated) | set | # | ||
(prototype-associated) | async | # | ||
(prototype-associated) | * | # | ||
(prototype-associated) | async | * | # | |
static | ||||
static | get | |||
static | set | |||
static | async | |||
static | * | |||
static | async | * | ||
static | # | |||
static | get | # | ||
static | set | # | ||
static | async | # | ||
static | * | # | ||
static | async | * | # |
Limitations of methods:
- Accessors can’t be async or generators.
Under the hood #
It’s important to keep in mind that with classes, there are two chains of prototype objects:
- The instance chain which starts with an instance.
- The static chain which starts with the class of that instance.
Consider the following JavaScript (not TypeScript!) example:
The two prototype chains look as follows:
More information #
- Public fields, private fields, private methods/getters/setters (blog post)
- All remaining JavaScript class features (chapter in “JavaScript for impatient programming”)
Non-public data slots in TypeScript #
By default, all data slots in TypeScript are public properties. There are two ways of keeping data private:
- Private properties
- Private fields
We’ll look at both next.
Note that TypeScript does not currently support private methods.
Private properties #
Any property can be made private by prefixing it with the keyword private
(line A):
We now get compile-time errors if we access that property in the wrong scope (line A):
However, private
doesn’t change anything at runtime. There, the property .name
is indistinguishable from a public property:
We can also see that private properties aren’t protected at runtime when we look at the JavaScript code that the class is compiled to:
Private fields #
Since version 3.8, TypeScript also supports private fields:
That code is mostly used the same way as the other version:
However, this time, the data is completely safe. Using the private field syntax outside classes is even a JavaScript syntax error (which is why we have to use eval()
in line A, so that the code runs):
The compilation result is much more complicated now:
This code uses a common technique for keeping instance data private:
- Each WeakMap implements one private field.
- It associates instances with private data.
For more information on this technique, see “JavaScript for impatient programmers”.
Private properties vs. private fields #
- Downsides of private properties:
- We can’t reuse the names of private properties in subclasses (because the properties aren’t private at runtime).
- No protection at runtime.
- Upside of private properties:
- Clients can circumvent the protection and access private properties. This can be useful if someone needs to work around a bug. In other words: Being completely protected has pros and cons.
Protected properties #
Private properties can’t be accessed in subclasses (line A):
We can fix the previous example by switching from private
to protected
in line A (we also switch in line B, for consistency’s sake):
Private constructors #
We can also make constructors private. That is useful when we have static factory methods and want clients to always use those methods, never the constructor directly. Static methods can access private instance members, which is why the factory methods can still use the constructor.
In the following code, there is one static factory method DataContainer.create()
. It sets up instances via asynchronously loaded data. Keeping the asynchronous code in the factory method enables the actual class to be completely synchronous:
In real-world code, we would use fetch()
or a similar Promise-based API to load data asynchronously in line A.
Right now, the private constructor prevents DataContainer
from being subclassed. If we want to allow subclasses, we can use protected
.
Typescript Abstract Functions
Initializing instance properties #
Strict property initialization #
If the compiler setting --strictPropertyInitialization
is switched on (which is the case if we use --strict
), then TypeScript checks if all declared instance properties are correctly initialized:
Either via assignments in the constructor:
Or via initializers for the property declarations:
However, sometimes we initialize properties in a manner that TypeScript doesn’t recognize. Then we can use exclamation marks (definite assignment assertions) to switch off TypeScript’s warnings (line A and line B):
Example: setting up instance properties via objects #
In the following example, we also need definite assignment assertions. Here, we set up instance properties via the constructor parameter props
:
Notes:
- In line B, we initialize all properties: We use
Object.assign()
to copy the properties of parameterprops
intothis
. - In line A, the
implements
ensures that the class declares all properties that are part of interfaceCompilerErrorProps
.
Making constructor parameters public
, private
, or protected
#
If we use the keyword public
for a constructor parameter, then TypeScript does two things for us:
- It declares a public instance property with the same name.
- It assigns the parameter to that instance property.
Therefore, the following two classes are equivalent:
If we use private
or protected
instead of public
, then the corresponding instance properties are private or protected (not public).
Abstract classes #
Two constructs can be abstract in TypeScript:
- An abstract class can’t be instantiated. Only its subclasses can – if they are not abstract, themselves.
- An abstract method has no implementation, only a type signature. Each concrete subclass must have a concrete method with the same name and type signature as that abstract method.
- If a class has any abstract methods, it must be abstract, too.
The following code demonstrates abstract classes and methods.
On one hand, there is the abstract superclass Printable
and its helper class StringBuilder
:
On the other hand, there are the concrete subclasses Entries
and Entry
:
And finally, this is us using Entries
and Entry
:
Notes about abstract classes:
- They can be seen as interfaces with bundled implementations.
- However, a class can only extend one abstract superclass but implement multiple interfaces.
- “Abstractness” only exists at compile time. At runtime, abstract classes are normal classes and abstract methods don’t exist (due to them only providing compile-time information).
- Abstract classes can be seen as templates where each abstract method is a blank that has to be filled in (implemented) by subclasses.
A TypeScript Abstract class is a class which may have some unimplemented methods. These methods are called abstract methods. We can't create an instance of an abstract class. But other classes can derived from abstract class and reuse the functionality of base class.
TypeScript Interface vs Abstract Class
Interface | Abstract Class |
---|---|
All members are abstract. | Some members are abstract and some are fully implemented. |
Interfaces support multiple inheritances. | Abstract class does not support multiple inheritances. |
TypeScript Interface has zero JavaScript code that means it is only available in TypeScript and does not produce any code in compiled JavaScript file. | Abstract class compile to JavaScript functions. |
Interfaces are generic in nature. They can be implemented by any class for example IClone interface can be implemented by any class like business objects, database classes. | Abstract classes are related. For example ViewModelBase is an abstract, class then we know this class will only inherits by ViewModels. |
Syntax
In above example, we have created an abstract class. First method doWork is abstract and we put abstract keyword before the method name. Abstract method does not have any implementation. Second method workStarted has implementation and it is not an abstract method.
Constructor
Typescript Implement Abstract Class
In abstract class, we can write constructor which automatically executes when user creates a new instance of derived class. Constructor function always has same name constructor and it may have parameters. Below is the constructor example:
Constructor is always used to initialize variables values and object setup work. In above, we have a constructor of BaseEmployee which takes two parameters firstName and lastName. In constructor, we have assigned parameter values to our variables.
Derived Cass
In above example, we have created an abstract class BaseEmployee. We have one abstract method doWork which we do not provide any implementation. Employee class extends BaseEmployee class. In Employee class constructor we call BaseEmployee constructor using super method. super method is used to call base class constructor. super method takes the same parameters as defined in base class constructor.
In last two lines, we have created an instance of Employee class and call doWork method.
Implement Interface
An abstract class can implement one or more interface. You can learn more about interfaces here. Below is the example of implementing interface by abstract class.
In above example, we have created two interfaces IName and IWork. Both interfaces are implemented by BaseEmployee abstract class by using implements keyword in highlighted line 10. Employee class extends BaseEmployee class. In line 32, we have created an emp variable of interface IWork and assign a new instance of Employee class. Now only member available in emp variable is doWork as emp variable data type is interface IWork.