38

How can one loop through the properties of a class in TypeScript? Take the following class for example:

export class Task implements Itask {
  public Id: number = 0;
  public Name: string;
  public Description: string;
  public Completed: boolean = false;
  public TaskType: TaskType;
}

Im want to retrieve the properties, hence: ["Id", Name", "Description", "Completed", "TaskType"]

Tried

GetTaskHeaders = () => {
  const _self = this;
  const tHead = $('<thead />').append('<tr />');

  for (let i = 0; typeof TodoApp.Task.arguments; i++) {
    const th = $('<th />');
    th.append(TodoApp.Task.arguments[i]);
    tHead.append(th);
  }

  console.log(tHead);

  return tHead;
};  

Unfortunately without success, i know using "TodoApp.Task.arguments" is incorrect. However, can someone show me the right way please?

Lin Du
  • 88,126
  • 95
  • 281
  • 483
meji
  • 1,041
  • 3
  • 11
  • 12
  • so far non of the below answers are sufficient to the actual question here – Tchakabam Mar 15 '21 at 16:53
  • 1
    @Tchakabam "You can't" is probably the correct answer, then. – Roy J Mar 15 '21 at 17:25
  • I was hoping for anything different, but I read up on the official TypeScript handbook, and the answer indeed is, it's not possible. – Tchakabam Mar 15 '21 at 20:04
  • The problem here is, the question doesn't match the answers, while they might have been useful to the OP :) The question is a Typescript specific one, and has the answer I gave in my previous comment afaiu from recent research. While the original OPs purpose might actually have been satsified with the below answers, those are answers to JS specific questions (https://stackoverflow.com/questions/684672/how-do-i-loop-through-or-enumerate-a-javascript-object). – Tchakabam Mar 15 '21 at 20:07
  • see my more type-centric answer below ;) – Tchakabam Mar 15 '21 at 20:49

4 Answers4

15

Let's consider that all "not defined" properties i.e. all properties that are defined in the typescript class like (I wrote "not defined" and not undefined for a reason that will be clear below)

class A { 

   prop1: string
   prop2: number

} 

will not be enumerated by any of Object.keys or this.hasOwnProperty(k) since the autogen javascript has no knowledge of those properties. You have only one option, when you create your typescript class, that is to initialize all properties to default values like

class A { 

   prop1: string
   prop2: number
   prop3: B

   constructor() {
     this.prop1="";
     this.prop2=-1;
     this.prop3=null;
   }

} 

At this point you will get all the properties of A instance like in this mapping iteration from a dictionary

var a = new A();
for (var i in properties) {
   if (a.hasOwnProperty(i)) {
      a[i]=properties[i];
   }
}

If you don't like the default values solution, you can still do this using the magic undefined javascript keyword so that you do:

class A { 

   prop1: string = undefined
   prop2: number = undefined

} 

At this point the javascript counterpart will have all properties in the model and you will iterate them with Object.keys(this) or inspect them via the this.hasOwnProperty

loretoparisi
  • 15,724
  • 11
  • 102
  • 146
  • 8
    I wish there was a way to have typescript do the `undefined` initializing automagically. – Adam Keenan Feb 04 '17 at 01:10
  • 8
    The basic idea's right but the code's wrong isn't it? The 'properties' variable is not defined. You need something like: `var a = new A(); for(var i in a) { if(a.hasOwnProperty(i)) {console.log(a[i]);}}` – Rich N Oct 05 '17 at 18:37
  • this doesn't answer the question, which is rather about using the type-declaration during runtime than doing runtime object-props iteration in plain JS. – Tchakabam Mar 15 '21 at 16:53
11

See How do I loop through or enumerate a JavaScript object?

In your case, something like:

for (var i in TodoApp.Task) {
    if (TodoApp.Task.hasOwnProperty(i)) {
        var th = $('<th />').append(TodoApp.Task[i]);
        tHead.append(th);
    }
}
Community
  • 1
  • 1
Roy J
  • 42,522
  • 10
  • 78
  • 102
  • 2
    The code only works if one creates a new task, ie. var tsk = new TodoApp.Task(...); I thought you could iterate through the type structure like it was static? – meji Jun 22 '15 at 18:41
  • 2
    No, you can only iterate through objects. The constructor is a function (if you look at the JavaScript that is generated by TypeScript, it is clearer). – Roy J Jun 22 '15 at 19:35
  • 7
    if you have `class A { prop1:string }` and you do `a = new A()`, without assigning `A.prop1`, this will not exist on the object instance `A`, so the `hasOwnProperty` will return `false`. In fact the autogen javascript has nothing to do with typescript properties definition in the class object. – loretoparisi Sep 07 '16 at 09:11
  • Won't typescript complain here that TodoApp.Task is not indexable? – ORcoder Jul 22 '19 at 21:46
  • @ORcoder if TodoApp.Task is a class, yes. If it's an instance, no. In my answer, I was treating it as an instance (hence "something like"). – Roy J Jul 23 '19 at 20:34
  • 1
    this doesn't answer the question, which is rather about using the type-declaration during runtime than doing runtime object-props iteration in plain JS. – Tchakabam Mar 15 '21 at 16:53
2

Here is my answer, based on the question interpreted as in:

How to iterate during runtime on the enumeration of the (public) properties of a TypeScript declared class or other declared interface (or even object-type) ?

And the question could be varied into using private class members, given they would be accessible in either scope.

Answer: can't do. Unfortunately. The only thing you can do to get close to it is the following. And this will hopefully explain the ambiguity between the Q/As here, and what some TS devs might really want to do:

(Check/run this code below on the TS playground here)

interface IFoo {
   firstProp: number
   secondProp: number
}

class Foo implements IFoo {
    readonly firstProp = 100;
    readonly secondProp = 200;
    someOtherProp = "bar";
}

enum IFooProps {
  firstProp,
  secondProp
}

for (key in Object.keys(IFooProps)) { // or at best IFoo, not needing to use an enum
  // ... do something with interface keys
}

Note there isn't any way to check at compile-time so far IFooProps and IFoo actually match (TS feature request...).

And also, the above doesn't work so well, because keys of the enum object also includes its values ...

However, now to fix that, we could do:

const foo = new Foo();
Object.keys(IFooProps).forEach(prop => {
  if (!isNaN(Number(prop))) return;
  console.log(foo[prop]) // prints value of all IFoo implementations
});

// OR (this is just a "nicer" way to effectively run the same code as above here ^)

// can only be done with "runtime alive" objects (enums are, but class/interface are types not objects, 
// but enums are also types too ;))
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
    return Object.keys(obj).filter(k => Number.isNaN(+k)) as K[];
}

enumKeys(IFooProps).forEach(prop => {
    console.log(foo[prop]) 
})

If TypeScript would allow to check that IFooProps actually enumerates the properties of IFoo (or the compiler would allow to declare/generate such an enum from any declared interface, explicitly or implicitly), then this would actually be type-safe (and convenient).

At the moment, this is rather a clumsy workaround, needing to declare the interface "twice" (redundantly in the enum).

Some credit to: https://www.petermorlion.com/iterating-a-typescript-enum/ (For helping out on the enumKeys helper)

EDIT:

Another possible interpretation of this question (or to make sure to clarify with respect to what we said above):

Using enumerations of interface keys at compile-time (only)

Yes it is possible to enumerate on interfaces, BUT not at runtime:

(run this in TS playground here)

interface IBar {
  firstThing: [string, string]
  secondThing: [number, number]
}

function doThatWithTypedThing<Key extends keyof IBar>(someDeclaredThingKey: Key, someDeclaredThing: IBar[Key]) {
  console.log(someDeclaredThingKey, someDeclaredThing)
}

const bar: IBar = {
  firstThing: ["bli", "bla"],
  secondThing: [400, 500]
}

doThatWithTypedThing("firstThing", bar.firstThing) // prints: firstThing [ "bli", "bla" ]
doThatWithTypedThing("secondThing", bar.secondThing) // prints: secondThing [ 400, 500 ]
doThatWithTypedThing("firstThing", bar.secondThing) // <- compiler error 2345
doThatWithTypedThing("notFirstThing", bar.secondThing) // <- other compiler error 2345
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Tchakabam
  • 494
  • 5
  • 11
1

I had a similar question when migrating from javascript to typescript and my javascript code was looping over the properties of an object.

The older answers give type safe solutions, this answer is not type safe and just avoids the typescript errors using type casting. Which means typescript will not generate an error if you assign invalid properties or assign a value with the wrong type to a property.

Example javascript that loops over properties:

function loop_over_props(object) {
  for (let k in object)
    console.log(object[k]);
}

The typescript that does the same thing :

interface HashMap<T> {
  [key: string]: T;
};

type SomeType = {
  field1: number;
  field2: string;
  field3: number;
};

function loop_over_props(object: SomeType) {
  var casted_object: HashMap<any> = <HashMap<any>>object;
  for (let k in casted_object)
    console.log(casted_object[k]);
}

For cases like mine, where you are migrating existing javascript and don't have many cases of needing unsafe code, then converting code to typescript probably has enough benefits to accept a few unsafe cases.

tsmigiel
  • 307
  • 3
  • 8