40

I'd like to create a generic TypeScript class for rendering (as a HTML list) of an array of objects which implement a specific interface.

e.g.

class GenericListRenderer<T> {
  items: T[];

 constructor(listItems: T[], className?: string){
      this.items = listItems;
      ...
    }

    private getPropertyNames(): string[]{

    // What is the best way to access all property names defined in
    // TypeScript interface 'T' that was used in this generic?
    ...   
    }

    render(){
      var propNames: string[] = this.getPropertyNames();
      // render list with each item containing set of all 
      // key(prop name)/value pairs defined by interface 'T'  
      ...

    }
}

Q: what would be the best way to get a 'compile-time' list of all property names defined in the specified () TypeScript interface?

Like C++ templates, I believe that TypeScript could resolves these generics during "compile time", when TypeScript type information (like an interface supplied to the generic used to instantiate a particular object) is readily available.

Since all the required type information is potentially supplied, I was just curious if there was a TypeScript extension/facility available to access this info w/o excessive runtime filtering of 'vanilla' Javascript objects -- which might be problematic due to ambiguous inheritance issues (e.g. desired TypeScript inherited interface properties may get filtered out if runtime, generic, Javascript (obj.hasOwnProperty(prop)) is used for filtering properties).

This useful property introspection potential could be unambiguously resolved with TypeScript's super-set of type meta-data during 'compile-time' vs trying to resolve this information in the translated Javascript, when all of this type information is discarded.

I'd hate to 'reinvent-the wheel' with a potentially imperfect Javascript hack if a standard (TypeScript) approach exists.

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
user3413621
  • 501
  • 1
  • 4
  • 6
  • At runtime it is plain JavaScript so you are actually looking for a JavaScript method to get all properties ... and that is answered here http://stackoverflow.com/questions/85992/how-do-i-enumerate-the-properties-of-a-javascript-object – Markus May 13 '15 at 07:02

3 Answers3

38

This is possible by using custom transformers introduced by https://github.com/Microsoft/TypeScript/pull/13940, which is available in typescript >= 2.4.1.

My npm package, ts-transformer-keys, is a good example.

import { keys } from 'ts-transformer-keys';

interface Props {
  id: string;
  name: string;
  age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']
kimamula
  • 11,427
  • 8
  • 35
  • 29
  • 4
    This look good, but it seems to require a special build procedure - is that correct? It would also be great if there were an `optionalKeys` function as well - which returned the optionals. This would make it possible to validate required props runtime, in a typesafe manner. – Jørgen Tvedt May 21 '17 at 07:29
  • 1
    Right. There is an issue requesting a plugin support for custom transformers: https://github.com/Microsoft/TypeScript/issues/14419. It's not difficult to implement the `optionalKeys` function with custom transformers. – kimamula May 22 '17 at 07:19
  • @kimamula seems interesting, do you know in what version it is introduced? – Tom Smykowski Jul 04 '19 at 16:46
  • 1
    @TomaszSmykowski I forgot to update my answer. The functionality was introduced in TypeScript 2.4.1. – kimamula Jul 05 '19 at 15:30
  • 5
    important to know that `keys();` should be invoke in the same file as the interface defined. Spent on this a few hours.. :) – Raz Ronen Jul 16 '20 at 23:19
  • Worth pointing out that I have spent long hours trying to make this work and [I'm not the only one](https://github.com/kimamula/ts-transformer-keys/issues/4). There are LOTS of caveats to getting this to work with various build chains. I can't seem to make this work with babel at all. – Liam Dec 14 '22 at 11:23
  • For nested properties check this package: https://www.npmjs.com/package/ts-interface-keys-transformer More info: https://stackoverflow.com/a/75876403/4735052 – p__d Mar 29 '23 at 11:07
11

This is not possible to retrieve that information at runtime, and they will never be per default possible unless you store them during the compilation. The solution is therefore be in reflection using ReflectDecorators.

Here is an excellent article covering the question of retrieving compilation time metadata at the runtime. In short: you add a decorator to the interface you would like to keep the description, this one will be converted to a JSON object which one will be stored into the code itself. During the runtime, you will be able to retrieve this JSON object having all the interface data. This is now experimental (11th Feb 2016) but in a good way.

Note: The reason why it will never by per default is basically a choice of design for TS not to overload the js code with metadata (unlike Dart).

Flavien Volken
  • 19,196
  • 12
  • 100
  • 133
  • 3
    _"unless you store them during the compilation"_: as a loosy implementation, I created a `static PROP_NAMES : string[] = ['prop1', 'prop2', 'propN'];` in `MyClass`. Then I do a `MyClass.PROP_NAMES.forEach(key=> { /* do something */ });` – Julien Kronegg Jan 25 '17 at 12:13
1

At runtime all of the type information is erased, so the best you can do is enumerate the properties of one of the objects. This will give you back all properties, even those that were not on the specified interface.

class GenericListRenderer<T> {

    constructor(private items: T[], private className?: string){

    }

    private getPropertyNames(): string[] {
        var properties: string[] = [];

        if (this.items.length > 0) {
            for (var propertyName in this.items[0]) {
                console.log(propertyName);
                properties.push(propertyName);
            }
        }

        return properties;
    }

    render(){
      var propNames: string[] = this.getPropertyNames();
    }

}

class Example {
    constructor(public name: string) {

    }
}

var example = new Example('Steve');

var a = new GenericListRenderer<Example>([example]);

a.render();

There is also Object.keys(), which gives you back all of the properties, although it is only supported in IE9 and above.

If you can supply more of a use case for what you want to do with the properties, it may be possible to give you an alternate solution using attributes or some other mechanism.

Fenton
  • 241,084
  • 71
  • 387
  • 401
  • that's the rub: it seems that a runtime solution would not take advantage of the discarded typeface meta-data, which leads me to believe that only a 'compile-time' TypeScript solution would do the trick. Since I'm not a TypeScript guru: just wondering if such a useful capability already existed. – user3413621 May 14 '15 at 18:59