2

I'm currently implementing an array-like typescript class. Is there a way to count the number of items in an indexed signature object in TypeScript? The class has got member functions and properties but should be accessible via the index signature. There is the following setup

export class MyIndexedClass {
  [index: number] : number;
  anotherProp = 'hello';

  get length() {
    // Object.keys returns an array of all properties as strings
    return Object.keys(this).length;
  }
}

I want to get the length of the indexed items without all the static items (functions and string properties). Maybe filtering for numeric keys would do the trick, but this seems to be error prone and I can't imagine that there is a more intelligent approach.

return Object.keys(this)
    .filter(o => !isNaN(parseInt(o, 10)))
    .length;

This is really hacky and I do not like this approach at all. Any ideas or suggestions on this? Thanks, cheers!

Felix Lemke
  • 6,189
  • 3
  • 40
  • 67

1 Answers1

1

So you want the length of something like {0: "first", 100: "last"} to be 2 and not 101, right? That is, the length of something like a sparse array is expected to be less than the value you'd get from its length property. There's no built-in method that will do that for you, so filtering properties is probably the easiest way to go.

Assuming so:

A better filter for "numeric" keys (which as you know are really strings) is something like this:

Object.keys(this).filter(k => (""+(+k))===k).length

The filter is coercing the key to a number and then back to a string... if it survives that roundtrip unscathed, then that key is indistinguishable from a numeric key. Otherwise it is not a true "numeric" key and you won't be able to access that property with a numeric index. For example:

const obj: { [k: number]: string | undefined } = {};
obj[1] = "A";
console.log(JSON.stringify(obj)); // {"1":"A"} 
obj[+1] = "B";
console.log(JSON.stringify(obj)); // {"1":"B"}
obj["1"] = "C";
console.log(JSON.stringify(obj)); // {"1":"C"} 
obj["+1"] = "D"; 
console.log(JSON.stringify(obj)); // {"1":"C","+1":"D"}

console.log(Object.keys(obj).filter(o => !isNaN(parseInt(o, 10)))); 
// Array [ "1", "+1" ]

console.log(Object.keys(obj).filter(k => ("" + (+k)) === k));
// Array [ "1" ]

Even though the numeric value 1 is the same as the numeric value +1, the string value "1" is distinct from the string value "+1". The object obj has properties at keys "1" and "+1" but you can only access the "1" property with a numeric index.

Note that a numeric index also doesn't restrict you to nonnegative whole number keys. All of the following is fine in TypeScript and in JavaScript:

obj[1.5] = "E";
obj[-2] = "F";
obj[Infinity] = "G";
obj[NaN] = "H";
obj[1.23e100] = "I";

And most of those fail the parseInt() test but pass the filter I've given. Similarly, keys like "23 skidoo" would fail the filter I've given but pass the parseInt() test.


Anyway, hope that helps. Good luck!


UPDATE: since you want this to act more like an actual array, the length property should always be greater than the highest numerical index. It looks like an array specifically only cares about numeric-compatible indices that correspond to non-negative integers less than some value (2^32 maybe). You can get something like that behavior with the following:

  get length() {
    return Math.max(-1, ...Object.keys(this).filter(
      k => ("" + (+k)) === k // numeric keys
    ).map(
      k => +k // as numbers
    ).filter(
      n => isFinite(n) && n >= 0 && n === Math.round(n) // non-neg integers
        && n < 4294967296 // less than 2^32
    )) + 1;
  }

You could simplify that a bit but it's more or less what you're looking for, I think.

If you'd rather come at this whole problem from a different angle you could make your class instances a Proxy that delegates numeric index accesses and length to a real array. Or maybe better, you can make your class a subclass of Array itself to inherit the behavior of length without having to reimplement it from scratch.

Good luck again.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Hi, thanks for your detailed answer! But this doesn't target the wanted solution. It is intended to have got an array-like behavior, for that, the assumption in your first sentence is not valid. – Felix Lemke Feb 05 '19 at 16:47
  • Updated with suggestion to get array-like `length` behavior. – jcalz Feb 05 '19 at 19:43