Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Last active March 26, 2023 15:26
Show Gist options
  • Save MirzaLeka/576983bbaff9af5450cc44fb26c0aa7f to your computer and use it in GitHub Desktop.
Save MirzaLeka/576983bbaff9af5450cc44fb26c0aa7f to your computer and use it in GitHub Desktop.

Extending Core Classes in C#, JavaScript & TypeScript

This article will teach your how to extend the core array class with custom methods in various languages. To apply this we'll make use of:

  • Extension Methods (C#)
  • Prototypes and Classes (JavaScript)
  • Types and Prototypes and Classes (TypeScript)

We're going to make a new method isEmpty that is going to check if the array, list or collection is empty. So when a consumer calls this method it will return true or false.

collection.isEmpty() // T / F

Doing this is super simple and we'll see how it looks like in different languages.

Extending Core Class in JavaScript using Prototypes

To extend Core Class in JavaScript we make use of the Prototype Inheritance Te prototype is like a tree where all properties and methods on the certain class/object exist.

const myArr: string[] = [];

To verify that this array is empty we can make use of length property

if (myArr.length === 0) // array is empty

To abstract this logic in it's own method we'll use the Prototype on the Array class

Array.prototype.isEmpty = function() {
  return this.length === 0;
}

Where:

  • Array.Prototype referrs to the Core Array Class
  • isEmpty is the name of the method we created
  • = function() creates a new function
  • It's important to use the function expression here as opposed to arrow function because we need to make use of this keyword
  • this keyword referrs to an array that will use the isEmpty method

Use cases:

1)

if (myArr.isEmpty()) // T / F

2)

const myArr = []
myArr.isEmpty() // True

3)

const myArr = [1, 2, 3]
myArr.isEmpty() // False

However, here we encounter an issue as this cannot be applied on collections like Set or Map as those classes do not use length property. They use size property.

So we'll need to come up with solution that can be applied to all cases (arrays, maps and sets)

Unified Solution for all Collections

Since arrays, sets and maps are all inheriting from Object class in JavaScript, the easiest way to apply unified solution for all three is to extend the Object class and write logic for each case.

Here we also need to check if consumer is array or map/set or other type.

Object.prototype.isEmpty = function () {

  if (this instanceof Array) {
    return this.length === 0; // in case object is array use length property
  }

  if (this instanceof Map) {
    return this.size === 0; // in case object is map use size property
  }

  if (this instanceof Set) {
    return this.size === 0; // in case object is set use size property
  }

  return; // in neither case return undefined
};

The solution above works for arrays.

const myArr = [];

console.log(myArr.isEmpty()); // True

And it also works for Map and Set collections:

const myMap = new Map();
const mySet = new Set();

console.log(myMap.isEmpty()); // True
console.log(mySet.isEmpty()); // True

The final return is super important as there are other classes in JS that inherit from Object base class. Extending the Object class with isEmpty method means that every object has that property and we do not want that. There is no point of getting the length of a Date class or similar instances.

const d = new Date();
console.log(d.isEmpty()) // undefined

Extending Core Class in TypeScript using Prototypes

To use Prototypes in TypeScript we need to do some additional work.

For starter if we do this in TypeScript the compiler will complain that isEmpty method does not exist on type of array.

Array.prototype.isEmpty = function () {
  return this.length === 0;
};

// Property 'isEmpty' does not exist on type 'any[]'.ts(2339)

To solve this issue we need to create a .d.ts file where we'll specify rules for types we'll use. Basically we'll create isEmpty method for each type.

// index.d.ts file
export {};

declare global {
  interface Array<T> {
    isEmpty(): boolean;
  }
}

declare global {
  interface Set<T> {
    isEmpty(): boolean;
  }
}

declare global {
  interface Map<K, V> {
    isEmpty(): boolean;
  }
}

Then we just create index.ts file and make use of the prototype. Note we do not import d.ts file.

// index.ts file
Array.prototype.isEmpty = function () {
  return this.length === 0;
};

Set.prototype.isEmpty = function () {
  return this.size === 0;
};

Map.prototype.isEmpty = function () {
  return this.size === 0;
};

const myArr = [];

console.log(myArr.isEmpty());

const myMap = new Map();
const mySet = new Set();

console.log(myMap.isEmpty());
console.log(mySet.isEmpty());

I seprated each prototype here on purpose due to TypeScript compiler complaining about many overrides of Object class. It also looks clearer this way.

Alternative Solution for extending Core Classes JS/TS

So far we've been using inheritance by Prototype, but both JavaScript and TypeScrpt also support class inheritance (that uses prototypes under the hood).

For example we can create a custom array class called ExtendedArray that has all properties and methods of an Array class, with our additional properties.

JavaScript Solution

class ExtendedArray extends Array {
  constructor() {
      super();
  }
  isEmpty() {
      return this.length === 0;
  }
}

To make use of the isEmpty method we need to create an instance of ExtendedArray class.

const myArr = new ExtendedArray();
console.log(myArr.isEmpty()); // True

myArr.push('Hello')
console.log(myArr.isEmpty()); // False

The same goes for the other two collections.

class ExtendedMap extends Map {
  constructor() {
      super();
  }
  isEmpty() {
      return this.size === 0;
  }
}

const myMap = new ExtendedMap();
console.log(myMap.isEmpty()); // True

myMap.set('Hello', 'World')
console.log(myMap.isEmpty()); // False

//

class ExtendedSet extends Set {
  constructor() {
      super();
  }
  isEmpty() {
      return this.size === 0;
  }
}

const mySet = new ExtendedSet();
console.log(mySet.isEmpty()); // True

mySet.add('Hello')
console.log(mySet.isEmpty()); // False

//

class ExtendedArray extends Array {
  constructor() {
      super();
  }
  isEmpty() {
      return this.length === 0;
  }
}

const myArr = new ExtendedArray();
console.log(myArr.isEmpty()); // True

myArr.push('Hello')
console.log(myArr.isEmpty()); // False

TypeScript Solution

To apply this logic in the world of TypeScript we need to add generic types to TypeScript classes.

class ExtendedMap<K, V> extends Map<K, V> {
  constructor() {
      super();
  }
  isEmpty() {
      return this.size === 0;
  }
}

const myMap = new ExtendedMap();
console.log(myMap.isEmpty()); // True

myMap.set('Hello', 'World')
console.log(myMap.isEmpty()); // False

//

class ExtendedSet<T> extends Set<T> {
  constructor() {
      super();
  }
  isEmpty() {
      return this.size === 0;
  }
}

const mySet = new ExtendedSet();
console.log(mySet.isEmpty()); // True

mySet.add('Hello')
console.log(mySet.isEmpty()); // False

//

class ExtendedArray<T> extends Array<T> {
  constructor() {
      super();
  }
  isEmpty() {
      return this.length === 0;
  }
}

const myArr = new ExtendedArray();
console.log(myArr.isEmpty());

myArr.push('Hello')
console.log(myArr.isEmpty());

Add method that takes a parameter

Looking at the examples above you may noticed that three different collections use different ways to insert data:

  • push in Array
  • set in Map
  • add in Set

Using the same logic we added above we could create a unified method (insert) that would work for all three.

class ExtendedMap<K, V> extends Map<K, V> {
  constructor() {
    super();
  }
  isEmpty() {
    return this.size === 0;
  }
  insert(key: K, value: V) { // where K,V are to types pased to insert (e.g. string, string)
    this.set(key, value)
  }
}


const myMap = new ExtendedMap<string, string>(); // where K,V are strings
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World')
console.log(myMap.isEmpty()); // False

//

class ExtendedSet<T> extends Set<T> {
  constructor() {
    super();
  }
  isEmpty() {
    return this.size === 0;
  }

  insert(value: T) {
    this.add(value)
  }
}

const mySet = new ExtendedSet<string>(); // where T is string
console.log(mySet.isEmpty()); // True

mySet.insert('Hello')
console.log(mySet.isEmpty()); // False

//

class ExtendedArray<T> extends Array<T> {
  constructor() {
    super();
  }
  isEmpty() {
    return this.length === 0;
  }
  insert(value: T) {
    this.push(value);
  }
}

const myArr = new ExtendedArray<string>(); // where T is string
console.log(myArr.isEmpty());

myArr.insert('Hello')
console.log(myArr.isEmpty());

Extending the prototype

There first thing we need to do is update the index.d.ts file.

export {};

declare global {
  interface Array<T> {
    isEmpty(): boolean;
    insert(value: T): void
  }
}

declare global {
  interface Set<T> {
    isEmpty(): boolean;
    insert(value: T): void
  }
}

declare global {
  interface Map<K, V> {
    isEmpty(): boolean;
    insert(key: K, value: V): void
  }
}

The next thing to is extend each prototype.

Array.prototype.insert = function<T> (value: T) {
  this.push(value);
};

Set.prototype.insert = function<T> (value: T) {
  this.add(value);
};

Map.prototype.insert = function<K, V> (key: K, value: V) {
  this.set(key, value)
};

Let's put this to the test:

const myArr = [];
console.log(myArr.isEmpty()); // True

myArr.insert('Hello');
console.log(myArr.isEmpty()); // False

//

const myMap = new Map();
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World');
console.log(myMap.isEmpty()); // False

//

const mySet = new Set();
console.log(mySet.isEmpty()); // True

mySet.insert('Hello');
console.log(mySet.isEmpty()); // False

For JavaScript is the same thing just without the types.

Extending Core Class in C#

Let's use what've just learned in JavaScript and apply it to C#.

Arrays

To verify the size of the array we use the Length property.

string[] myArr = new string[0];
Console.WriteLine(myArr.Length); // 0

To abstract this method like we did with Prototypes in JavaScript, we'll make use of the Extension Methods.

This is how we make IsEmpty method in C# for the arrays.

public static class Utils
{
	public static bool IsEmpty(this Array collection) // this referrs to the consumer array
	{
		return collection.Length == 0;
	}
	
}

Let's use it:

using System;
					
public class Program
{
	public static void Main()
	{
		var myArr = new string[0];
		Console.WriteLine(myArr.IsEmpty()); // True
    
	}
}

Lists

In case of lists, we use the Count method to verify the size.

using System;
using System.Collections.Generic;

public class Program
{
	public static void Main()
	{
  
	var myList = new List<string>();
    	Console.WriteLine(myList.Count == 0); // True
	}
}

To turn this into an Extension Method it would look like this:

public static class Utils
{
	public static bool IsEmpty<T>(this List<T> collection) // this referrs to the list that will call this method
	{
		return collection.Count == 0;
	}
	
}

Let's use it:

using System;
using System.Collections.Generic;

public class Program
{
	public static void Main()
	{
		var myList = new List<string>();
    	Console.WriteLine(myList.IsEmpty()); // True
    
	}
}

Unified Solution for C# Collections

Once again we want to create a unified solution that would work for all collections. On the positive side of things, C# has the same core class for all collections called Enumerable and the interface IEnumerable.

With this in mind we can use IEnumerable for collections instead of been specific if it is an array, list, dictionary, etc.

Let's update our Utils class.

public static class Utils
{
	public static bool IsEmpty<T>(this IEnumerable<T> collection)
	{
		return collection != null && !collection.Any(); // Any() method is available on all IEnumerables
	}

The solution above will work for both lists and arrays as seen below.

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
	public static void Main()
	{
  		var myList = new List<string>();
    	Console.WriteLine(myList.IsEmpty()); // True
    
	  	myList.Add("Hello");
    	Console.WriteLine(myList.IsEmpty()); // False
    
      //
    
	  	string[] myArr = new string[0];
    	Console.WriteLine(myArr.IsEmpty()); // True
    
	  	myArr = new string[5]; // array of Nulls
    	Console.WriteLine(myArr.IsEmpty()); // False
    
      //
    
      var myDict = new Dictionary<string, string>();
   		Console.WriteLine(myDict.IsEmpty()); // True
    
	  	myDict.Add("Hello", "World");
    	Console.WriteLine(myDict.IsEmpty()); // False
	}
}

Extension method with paramters

To be able to pass parameters to collection we simply need to add them to methods we're creating.

public static class Utils
{

	public static bool IsGreaterThan<T>(this List<T> collection, int size) // size is the parameter that we pass
	{
			return collection != null && collection.Count > size;
	}
	
}

Consumer Class

using System;
using System.Collections.Generic;

public class Program
{
	public static void Main()
	{
		var myList = new List<string>();
    Console.WriteLine(myList.IsGreaterThan(100)); // False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment