Extending Core classes in JavaScript, TypeScript & C# .NET
Extending Core classes in JavaScript, TypeScript & C# .NET
Photo by Pixabay from Pexels - My own edit
This article will teach you how to add custom methods and properties
to built-in
JavaScript/TypeScript
classes using Prototype & Class-based inheritance and C# Extension Methods.
We are going to create three custom methods that can be applied
to any type of collection, array, set, or map:
isEmpty
This method will lookup the length/size of the collection and
return true or false if the collection is empty or not
insert
This method will push a new item into a collection and return
an existing collection
tap
This method will take a callback function (from console.log to custom function)
that can do pretty much anything and return an existing
collection
This is going to work similarly to
Extension Methods in C#. Any consumer of the collection will be able to use
these methods as well as the default ones.
The downside of the original implementation is that different collection types use
different methods to interact with it. The things we're interested in are as follows:
Push new item into collection (array: push(), set: add(), map: set())
Inspect the size of the collection (array: .length, set, map: .size)
So instead of needing to remember which property applies to which collection,
we'll create custom unified methods that work for either.
Another important detail is the return type. If our custom method returns
an existing collection it means that we can append other methods from that collection to
it:
But if the method returns a different value,
we can no longer append a collection method to it.
This is super easy to implement, and we'll see what it looks like
with different paradigms, such as Prototype and Class-based inheritance,
and in different programming languages.
Photo by WallpaperCave
JavaScript
Prototype-based Inheritance
Prototype-based inheritance is a mechanism for object-oriented programming in JavaScript
that allows objects to inherit properties and methods from other objects. In JavaScript,
every object has an internal property called its prototype, which is a reference to another
object that serves as its prototype.
The prototype is like a tree where all properties and methods on a certain object exist.
For example, if we create an array, we can expand the prototype property to see all
methods
and properties available on the arrays.
Array prototype preview
To add a new method to this prototype we'll do something like this:
The important thing to notice is that we're using a function statement as opposed to
an array function. But why? - It's because we'll use this
keyword within the function and this will refer to the consumer of the
method.
We are going to use the same formula as above to extend each collection type
(array, set, map) with each of our custom methods (isEmpty, insert, tap).
# isEmpty
This method will check the length/size of the collection, and tell us
whether it's empty or not.
# insert
This method will push a new item into a collection.
This is the part where I said that we'll have a unified solution. Because as we
can see:
arrays use push() to append a new item
sets use add() to append a new item
maps use set() to append a new item
and now all three can use insert() to do the same thing for each.
# tap
The tap method will look through each element within an array and call our callback
function.
This callback function can be an action dispatcher function, console.log/warn/error,
or any custom function.
Use case:
Now let's test each method. Since we extended the original prototype,
we can call our new methods directly on the instances.
Class-based Inheritance
The basic premise will look like this. Our custom class will extend the base (Super)
class.
Upon extending such classes it's also important to call super() within
the constructor
of a derived (Sub) class. This is necessary because a derived class inherits all of
the properties and methods of its parent class, and the parent class may have its own
constructor logic that needs to be executed in addition to the constructor logic of the
derived class.
We're going to repeat this process for each of our target classes:
Array (ExtendedArray)
Set (ExtendedSet)
Map (ExtendedMap)
# Extended Array
# Extended Set
# Extended Map
To test this we'll need to create a new instance of each of our custom classes
as opposed to working with core classes (which we did previously).
We'll push a new item into a collection using our insert method,
inspect the collection using the tap method and then check if the collection is
empty,
using the isEmpty method.
And it worked. Now we can use these newly created methods whenever we're working
with these types of collections.
Let's learn how to put this together in TypeScript as well.
Image from WallpaperAccess.com
TypeScript
On the TypeScript side things will be mostly similar to those in JavaScript with a few
extra steps.
Class-Based inheritance
To apply similar class-based logic in the world of TypeScript we need to
add generic types to TypeScript classes.
# Extended Array
# Extended Set
# Extended Map
To reassure that this once again works we're going to test it the same way.
Prototype-Based Inheritance TypeScript
In order for Prototypes to work with TypeScript we need to
add additional functionalities. Here is why.
If we attempt to extend the Array prototype the same way we did in JavaScript,
the TypeScript compiler will complain that the isEmpty method
does not exist on the type of array.
To overcome this issue we need to create a type declaration file (d.ts)
where we'll specify rules for the types we'll use.
Basically, we'll create the isEmpty method for each type.
With this in place at the root of our project, we can create index.ts
file
and extend each prototype. Note we do not import a d.ts file.
Putting this to the test we should get the same results.
Photo by Suzy Hazelwood from Pexels
Abstracting Third-Party packages with extensions
Sometimes we want to add functionality to a code from a
third-party library in the codebase and use it frequently,
but we do not want to learn all the methods and properties the library exposes.
Essentially we could create an abstraction for the library we're using
so that the consumer/developer can use a method without knowing what it
does under the hood, and which libraries it calls.
Let's set up a famous JavaScript date library - Day.js
, in the project.
We'll make use of the relative time plugin built into Day.js to get
the offset between desired and current date.
We'll extend the JavaScript Date class with our custom method (getDateOffset)
that abstracts the call to relative time in Day.js.
This is basically adding an additional feature to the Date class in JavaScript.
Let's test our method.
To achieve this trick in C#, be sure to use Humanizer
Nugget Package. If you want to learn more about Day.js, be sure to read my article:
The
complete guide to Day.js.
Photo by WakeUpAndCode.com
C# Extension Methods
We mentioned C# on several occasions in the article so we might as well add some input
here too.
To extend the core classes as we did with the prototype in JavaScript,
we'll make use of C# Extension methods. The core idea is similar,
we create a custom class with a custom method (extension) that will do the job we tell
it.
A few things to keep in mind:
class and method must be static
the first parameter of the extension method specifies the type that
it is going to be operated on. This is the consumer of the extension.
the first parameter is preceded by this keyword. This is what
tells C# that this method is an extension to the provided type (in this case
string).
With this in place, we import our Utils class in the file
where we wish to use the extension and apply it;
Now let's apply this pattern to our three custom extensions.
It's important to note here that, C# also supports class-based inheritance, as we did
before.
# isEmpty
We'll start by creating an extension method.
The beauty of C# collections, is that they all implement the
IEnumerable
interface. You've might have encountered something like this before.
If we create a method that returns IEnumarable, it won't really matter whether the method
is returning Array, List, or HashSet (JavaScript Set); in either of those cases the
compiler won't complain.
It works with Dictionaries (JavaScript Map) too, as long as we proceed with
KeyValuePair<K, V>, because IEnumerable excepts a single (wrapper) type.
Back to our example. The isEmpty method will work with collections, so the parameter
passed
to our extension method will be of type IEnumerable<T>.
We then call .Any() method on the desired collection.
This returns true or false whether the collection is empty or not.
Let's put this to the test:
Passed with flying colors.
# insert
For this method - we're going to use Stack and List classes.
Here, we won't be using IEnumerable, as we cannot insert data into IEnumerable.
Instead, we'll use create and override a method that can work with List<T> and
Stack<T>.
The consumer of either class can now use the insert method,
without knowing the underlying implementation.
# tap
To create a callback function in C#, we'll make use of the
Action<T>
delegate.
Here we'll use a List and HashSet (that is equivalent to a Set collection in
Java/JavaScript).
Now let's test this:
This can be applied to dictionaries too:
Photo by Skitterphoto from Pexels
The beauty of making extensions
If I have not convinced you to use extensions thus far and this should put a pin to it.
Using the methods we created above we'll create an empty array and add items to it.
Now let's swap an Array class with a Set class.
And everything works as expected with zero code changes.
The difference in the output is that the Set collection returns only unique elements.
Photo by Monstera from Pexels
The difficulties of working with extensions
Let's talk about potential issues with creating custom extensions:
#1
The first thing to be aware of is that we need to have all consumers of the extension
be on board with the extension method they're calling. For example, if we're using
List<T> in C# and I want to combine it with Stack<T>, we can only use
methods
within extensions that are available in both.
And if the two do not share a common language then we can implement one method for each
class<T> as we did above for the insert method.
#2
Future language changes.
If we add a custom method in code and the same method is introduced by the
core language team (JS/C#) in the feature, it may create ambiguity that leads to broken
code.
#3
Another danger of creating custom prototypes is that we can break code that relies on
the default behavior of built-in objects. For example, if we modify the
String.prototype object to add a custom method,
any code that relies on the default behavior of
the String object could be affected and potentially break.
Similarly with the Extension Methods in C#. If the name of the extension method
conflicts with an existing property, it could cause unexpected behavior or errors.
Summary
Extensions are a fun way to introduce new features, abstract implementation,
and create unified solutions, but may cause issues if used recklessly.