Array.prototype for .NET developers

Array.prototype for .NET developers

posted in javascript on  •  • last updated on

The Array.prototype functions have been available for a long time but it’s since Arrow functions that “Linq lambda syntax”-like functionality is available in JavaScript.

This blog post explains the most common functions by comparing them to their C# equivalents.

A basic example:

// JavaScript
const result = [0, 1, 2, 3, null]
    .filter(x => x !== null)
    .map(x => x * 10)
    .sort((a, b) => b - a);

expect(result).toEqual([30, 20, 10, 0]);

// C#
var result = new int?[] {0, 1, 2, 3, null}
    .Where(x => x != null)
    .Select(x => x * 10)
    .OrderByDescending(x => x)
    .ToArray();

Whenever you want to write a for to loop over an array, the Array.prototype functions probably allow you to achieve the same result but in more functional and succinct manner.

Do note that the deferred execution we know from Linq does not apply to Array.prototype!

Comparison Table

C# JavaScript MDN Link
Select() map((cur, index, array): any) map
Where() filter((cur): boolean) filter
Contains() includes(value, fromIndex) includes
FirstOrDefault() find((cur): boolean): undefined | any find / findIndex
LastOrDefault() findLast((cur): boolean): undefined | any findLast / findLastIndex
All() every((cur): boolean): boolean every
Any() some((cur): boolean): boolean some
Concat() concat(arr: any[]): any[] concat
Skip(start).Take(start - end) slice(start = 0, end = length-1) slice
string.Join() join(separator = ‘,’) join
Array.IndexOf() findIndex((cur): boolean): -1 | number findIndex
ElementAt() at(index) (accepts negative indexes) at
Count() length: number length
SelectMany() flat(levels = 1) / flatMap(fn) flat / flatMap
GroupBy() Object.groupBy(arr, groupFn) groupBy
Distinct() filter((el, i, arr) => arr.indexOf(el) === i)  
Extension method forEach((cur): void): void forEach
     
Mutating in JS These are in place operations  
OrderBy() sort((a, b): number) sort / toSorted
Reverse() reverse() reverse / toReversed

Commonly used methods

Select = map

// C#
var result = enumerable.Select(x => x);

// JavaScript
let result = array.map(x => x);

// Using spread to avoid mutation
const input = [{k: 1, v: true}, {k: 2, v: false}];
const result = input.map(x => ({...x, v: !x.v}))

When mapping to an object without code block, you need to wrap your object between extra parentheses like

[0, 1].map(x => ({value: x}));

// Because without the extra parentheses
[0, 1].map(x => {value: x});
// --> [undefined, undefined]

// it is the same as writing:
[0, 1].map(x => {
  value: x; // No error, because JavaScript?
  return undefined;
});

Where, Distinct = filter

Where and filter behave pretty much exactly alike. Linqs Distinct we’ll need to implement ourselves.

const input = [0, 0, 1, 5, 5];
// Equals true when the first occurrence of the value is the current value
const result = input.filter((element, index, array) => array.indexOf(element) === index);
// Or: [...new Set(input)];
expect(result).toEqual([0, 1, 5]);

Aggregate, GroupBy, … = reduce

Linq has Sum, Min, Max, Average, GroupBy, etc. While JavaScript doesn’t have them, they can all be achieved trivially with reduce

Sum, Min, Max, Average:

const input = [0, 1];
const sum = input.reduce((total, cur) => total + cur, 0);
const average = sum / input.length;
const min = Math.min.apply(Math, input); // Old school
const min = Math.min(...input); // Using spread

Aggregate, GroupBy:

const input = [0, 1, 2, 3];

const result = input.reduce((acc, cur) => {
  if (cur % 2 === 0) {
    acc.even.push(cur);
  } else {
    acc.odd.push(cur);
  }
  return acc;
}, {even: [], odd: []});

expect(result).toEqual({even: [0, 2], odd: [1, 3]});

Since 2024, JS has a dedicated Object.groupBy:

const socks = [
  { name: 'JavaScript', type: 'lang' },
  { name: 'Angular', type: 'package' },
  { name: 'React', type: 'package' },
];

const grouped = Object.groupBy(socks, sock => sock.type);

expect(grouped).toEqual({
  lang: [{ name: 'JavaScript', type: 'lang' }],
  package: [
    { name: 'Angular', type: 'package' },
    { name: 'React', type: 'package' },
  ]
})

slice

Linq has First, Last, Skip, SkipLast, SkipWhile, Take, TakeLast, TakeWhile. JavaScript has slice.

// Shallow copy
const input = [0, 1, 2, 3];
expect(input.slice()).toEqual([...input]);

// Signature
slice(startIndex = 0, endIndex = length-1);

C# Ranges

var array = new[] { 1, 2, 3, 4, 5 };

Assert.Equal(new[] { 3, 4 }, array[2..^1]);
Assert.Equal(new[] { 1, 2 }, array[..^3]);
Assert.Equal(new[] { 3, 4, 5 }, array[2..]);
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, array[..]);

with

Since 2023, there is also with which returns a new array where one item is replaced with a new value by index.

const newArr = arr.with(index, newValue);

Mutations

These functions operate in place.
Since ECMAScript 2023, some have non-mutating alternatives!

OrderBy = sort and toSorted

  • Use toSorted if you need a new array for the sorted result.
  • Without compareFn the array is sorted according to each character’s Unicode code point value.
  • The compareFn should return a number:
    • -1 (or any negative number): a comes before b
    • 0: Equal
    • 1 (or any positive number): a comes after b
// Numbers
[10, 5].sort(); // [10, 5]: each element is converted to a string
[10, 5].sort((a, b) => a - b); // [5, 10]

// Strings
['z', 'e', 'é'].sort(); // ['e', 'z', 'é']
['z', 'e', 'é'].sort((a, b) => a.localeCompare(b)); // ['e', 'é', 'z']

// Dates
[d1, d2, d3].sort((a, b) => a.getTime() - b.getTime());

C#

  • The signature is a bit different: OrderBy(Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
  • OrderBy vs OrderByDescending: switch a and b
  • JS doesn’t have ThenBy chaining possibilities, you’ll have to do this yourself.

forEach

While forEach is not really doing any mutation by itself, it is often what it’s used for. Mutations are especially dangerous in for example a Redux environment where UI changes might lag. The same can usually be achieved with map.

const input = [{value: 1, visited: 0}, {value: 2, visited: 0}];

// Potentially dangerous
input.forEach(el => {
    el.visited++;
});

// ...el will create a shallow copy only!
const result = input.map(el => ({...el, visited: el.visited + 1}));

// Or use the good old loop?
for (let itm of input) {
    console.log(itm.value);
}

Other mutators

Linq for ages 5 and up

Thanks Wim De Cleen (and Martin Fowler) for this amazing diagram, I love it!

Explain LINQ to a 5 year old


Stuff that came into being during the making of this post
Other interesting reads
Updates
  • 1 June 2024 : Added Object.groupBy
  • 6 October 2023 : More updates for ECMAScript 2023
  • 4 September 2023 : Added flat and flatMap
  • 8 March 2023 : Added Distinct, flatMap
Tags: cheat-sheet tutorial