0

I know there are lots of 'LINQ' javascript libraries out there, but I just want a simple firstOrDefault.

I tried writing a prototype like this:

    Array.prototype.firstOrDefault = function<T>(this: Array<T>, predicate: (value: T, index: number) => boolean): T | undefined {
        return this.filter( predicate ).shift();
    }

And this would work, but the problem is, if I have something like this:

const myRows: MyType[] = [];
myRows.firstOrDefault<MyType>( t => t.Name == "Terry" );

I would like to have the <MyType> inferred. Is that possible?

UPDATE Reading the answer/disclaimer below, I have essentially the exact same code, but it doesn't work.

interface Array<T> {
    firstOrDefault<T>(predicate: (value: T, index: number) => boolean): T | undefined;
}

Array.prototype.firstOrDefault = function<T>(this: Array<T>, predicate: (value: T, index: number) => boolean): T | undefined {
    return this.filter( predicate ).shift();
}

// Usage
const contentRows = application.rble.getResultTable<ContentsRow>( tabDef, "contents" );
const configRow = 
    contentRows
        .firstOrDefault( r => r.section === "1" && KatApp.stringCompare( r.type, "table", true ) === 0 && r.item === tableName );

// Helper function...
getResultTable<T>( tabDef: TabDef | undefined, tableName: string): Array<T> {
    ...
}

But I get this error, where it doesn't know type of row... enter image description here

Terry
  • 2,148
  • 2
  • 32
  • 53

1 Answers1

2

DISCLAIMERS:

Disclaimer One: It is considered bad practice to modify built-in prototypes this way. It's unlikely anyone else will expect such a method to exist on Array.prototype, so at best it will be unnoticed by anyone other than you. At worst, other code that depends on the fact that such a method does not exist will eventually come along and things will break. For example, what if findOrDefault() makes it into ES2025 but it has different semantics or arguments from yours? It may be convenient and useful to use a paperclip instead of a fuse, but eventually such practices tend to cause more trouble than they prevent. To be fair, it looks like some people don't really think this is as big a deal as all that. See this question for the Stack Overflow conventional wisdom on the subject. In any case, from here on out I will assume that you are aware of these caveats and want to proceed anyway.


Disclaimer Two: There is already a built-in method on Array.prototype called find() which "returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfies the testing function, undefined is returned". That's what firstOrDefault() does, right? It seems therefore that your firstOrDefault() method is completely unnecessary, since you could just use find(). From here on out I will assume that you are aware of this and want to proceed anyway, for entertainment purposes only.


Disclaimer Three: The example code this.filter(predicate).shift() is not an efficient implementation. It goes through the effort of checking every single element of the array and collecing the ones which match the predicate, before throwing away all but the first such match. If there is a match is near the beginning of the array, you're doing a lot of unnecessary work... meaning that you will always get a worst-case performance. If you want an implementation that actually stops when it finds a match, you should use a loop like

const len = this.length;
for (let i = 0; i < len; i++) {
  const v = this[i]
  if (predicate(v, i)) return v;
}
return;

Or, you know, this.find(predicate). From here on out I will assume that you are aware of this issue and want to proceed anyway.

END DISCLAIMERS


TypeScript lets you use declaration merging to make modifications to existing interfaces. If your code is already in the global/ambient scope, then you can just re-open the Array<T> interface and add the method to it:

interface Array<T> {
  firstOrDefault(predicate: (value: T, index: number) => boolean): T | undefined;
}

If your code is in a module instead of global scope, you would have to use global augmentation and wrap your declarations in a declare global block:

declare global {
  interface Array<T> {
    firstOrDefault(predicate: (value: T, index: number) => boolean): T | undefined;
  }
}

Once you do this, the compiler will happily allow you to add the actual function to the prototype (and it infers the type of this and predicate for you):

Array.prototype.firstOrDefault = function (predicate) {
  return this.filter(predicate).shift();
}

and you can call the method on array objects without a compiler warning:

myRows.firstOrDefault(t => t.Name == "Terry");

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • You code indeed works, but I updated the question to show what I had. Basically exactly what you had, but for some reason it doesn't work. That said, thanks for the disclaimers. Especially 2 and 3. I will simply use `find`, but I wish I knew why my implementation didn't work. – Terry Feb 15 '21 at 03:44