C# List<T> Class to TypeScript extends Array<T>

C# List<T> Class to TypeScript extends Array<T>

How not to extend an Array in TypeScript

ยท

3 min read

After programming in C# for almost 19 years, I've decided to try my hand at TypeScript. Still thinking Object Oriented Programming concepts and C#, I've run into a few doozies I thought I'd document.

I tried extending my custom class from a List<T> with T being a custom class.

My C# Class

Below is my Team class in C#. At runtime, I expect to get a count of 2 for the number of enabled team members with my sample dataset.

public class Team: List<TeamMember> {

    public Team(List<List<string>> rows) {
        this.AddRange(rows.Where(x => !string.IsNullOrEmpty(x[0]))
          .Select(x => new TeamMember(x)));
    }

    public List<TeamMember> getEnabledUsers() {
        return this.FindAll(member => member.enabled);
    }
}

You can see a complete working sample in this fiddle.

TypeScript with a runtime error

I converted that C# class to a TypeScript class. This Team class compiles without any issues. But if you run it on TypeScript playground, you will see a runtime error.

...
class Team extends Array<TeamMember> {

    constructor(rows: Array<Array<string>>) {
        super();
        rows.filter(row => row[0] != "")
            .map(row => this.push(new TeamMember(row)));
    }

    getEnabledUsers() : Array<TeamMember> {
        return this.filter(member => member.enabled);
    }
}
...

Misleading runtime error

The error on the playground or even if you run it locally says:

[ERR]: "Executed JavaScript Failed:"

[ERR]: rows.filter is not a function

So that means that I have done something wrong in the constructor where rows.filter is. Trying to debug what happens in the constructor made me waste a few hours on a Sunday.

But, if I take away the getEnabledUsers() method, this error goes away. ๐Ÿคฏ

Reading through multiple SO questions and blogs etc finally led me to Object.values. The Object.values() method returns an array of a given object's own enumerable property values, in the same order as that provided by a for...in loop.

TypeScript which works

Object.values allowed me to reference this as an array in my Team class. So I changed return this.filter(member => member.enabled); to return Object.values(this).filter(member => member.enabled);. This made my code work at Runtime and gave me the necessary output as seen in this playground link.

Here's the Team class for comparison

...
class Team extends Array<TeamMember> {

    constructor(rows: Array<Array<string>>) {
        super();
        rows.filter(row => row[0] != "")
            .map(row => this.push(new TeamMember(row)));
    }

    getEnabledUsers() : Array<TeamMember> {
        return Object.values(this).filter(member => member.enabled);
    }
}
...

Is using Object.values() the correct thing to do?

Yes, using Object.values() fixed the runtime error. But it was extra computation and memory utilised to create an array from an already existing array. So IMHO, it seems like doubling the work.

What's the real error?

Thanks to my colleague Ynze who tracked down this FAQ answer from Microsoft on "Why doesn't extending built-ins like Error, Array, and Map work?".

So essentially, it's not supported in TypeScript for now due to how an Array for example uses ECMAScript 6's new.target to adjust the prototype chain.

TypeScript - Best Practice?

Speaking with another front-end developer colleague Ben resulted in him sharing the following code as better way of defining this Team class. You can see a working version on this playground link.

...
class Team {

    members = new Array<TeamMember>();

    constructor(rows: Array<Array<string>>) {
        rows.filter(row => row[0] != "")
            .map(row => this.members.push(new TeamMember(row)));
    }

    getEnabledUsers() : Array<TeamMember> {
        return this.members.filter(member => member.enabled);
    }
}
...

Verdict

TypeScript is not like C#. So proceed with caution ๐Ÿ˜‰ Hopefully this article will help the next TypeScript noob!

ย