C# List<T> Class to TypeScript extends Array<T>
How not to extend an Array in TypeScript
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!