2

I'm trying to figure out how to pass an array of propertynames (or fieldnames) of a given object, without using so called magic strings - because typos are easily made! In essence, I'm looking for something relevant to csharp's "Expression<>" I think.

E.g. with magic strings: searchFilter(model, 'searchParameter', ['id', 'name'])

E.g. typed, or how I would like to call the function: searchFilter(model, 'searchParameter', [m => m.id, m => m.name])


As a reference, this function looks a bit like this:

with magic strings: (or how I was trying to do it typed)

private searchFilter(mode: Model, q: string, properties: string[]): boolean {
   if (q === '') return true;

   q = q.trim().toLowerCase();

   for (let property of properties) {
     if (vacature[property.toString()].toString().toLowerCase().indexOf(q) >= 0) {
       return true;
     }
  }

  return false;
}

typed: (or how I was trying to do it typed, but this ofcourse just gives back functions.. I'd need a relevant 'function expression' as in C# to extract the called property, to get it's name)

private searchFilter(mode: Model, q: string, propertySelector: ((x: Model) => any | string)[]): boolean {
   if (q === '') return true;

   q = q.trim().toLowerCase();

   for (let property of propertySelector) {
     if (vacature[property.toString()].toString().toLowerCase().indexOf(q) >= 0) {
       return true;
     }
  }

  return false;
 }
Yves Schelpe
  • 3,343
  • 4
  • 36
  • 69
  • thanks, I'll look up and see what it does under the covers. Because - well if you need t do a lot of work it might not be as efficient. – Yves Schelpe Jun 24 '17 at 20:38
  • 1
    The names are substituted with strings at compile time, so there's no performance hit. Maybe a little during compilation itself, but I imagine it's pretty negligible. – JBC Jun 25 '17 at 05:52
  • true, this is a cool alternative - althout it requires post processing - it's a good approach I think if the library owners keep it maintained, feel free to post it as an answer! – Yves Schelpe Jun 25 '17 at 08:58
  • 1
    Sure, I went ahead and added it. – JBC Jun 25 '17 at 23:55

5 Answers5

2

You can't get rid of the string there is no such thing as nameof property in typescript (yet).

What you can do however is to type something as the key of another type.

Like this.

interface Model {
    a: string,
    b: number
}

function searchFilter(model: Model, q: keyof Model) { }

This results in:

searchFilter(null, 'a') // works
searchFilter(null, 'b') // works
searchFilter(null, 'c') // error c is not a property of Model

You can type an array of properties of a type like this:

function searchArray(model: Model, q: string, properties: Array<keyof Model>) { }

searchArray(null, 'blabla', ['a', 'b'])
toskv
  • 30,680
  • 7
  • 72
  • 74
  • thanks, I like this idea. I do have my own answer, which is slower I guess - since it has to parse it each time.... Thanks, nice tip! – Yves Schelpe Jun 24 '17 at 17:05
1

Nameof is not available natively, but the functionality has been replicated with a third-party library.

You can achieve nameof functionality through the use of a third-party library (https://www.npmjs.com/package/ts-nameof). You can view the source code here: https://github.com/dsherret/ts-nameof

In this case, the library provides a number of options based on what level of object name you want to use, such as the name of a variable itself, the name of a method, the name of a method plus its containing class, and so forth (excerpt from the library docs).

Below shows the transpiled JavaScript output on the left and the TypeScript equivalent on the right.

console.log("console");             // console.log(nameof(console));
console.log("log");                 // console.log(nameof(console.log));
console.log("console.log");         // console.log(nameof.full(console.log));
console.log("alert.length");        // console.log(nameof.full(window.alert.length, 1));
console.log("length");              // console.log(nameof.full(window.alert.length, 2));
console.log("length");              // console.log(nameof.full(window.alert.length, -1));
console.log("alert.length");        // console.log(nameof.full(window.alert.length, -2));
console.log("window.alert.length"); // console.log(nameof.full(window.alert.length, -3));

"MyInterface";                      // nameof<MyInterface>();
console.log("Array");               // console.log(nameof<Array<MyInterface>>());
"MyInnerInterface";                 // nameof<MyNamespace.MyInnerInterface>();
"MyNamespace.MyInnerInterface";     // nameof.full<MyNamespace.MyInnerInterface>();
"MyInnerInterface";                 // nameof.full<MyNamespace.MyInnerInterface>(1);
"Array";                            // nameof.full<Array<MyInterface>>();
"prop";                             // nameof<MyInterface>(o => o.prop);

These strings are substituted at transpilation time, so there should not be any runtime performance penalty.

JBC
  • 667
  • 1
  • 9
  • 21
  • FYI, here is the official `nameof` suggestion thread on github: https://github.com/Microsoft/TypeScript/issues/1579 It's something that's been on their radar for awhile, but there is still no proposed timeline. – JBC Jun 26 '17 at 22:05
1

It's possible to create closure methods with the same names as properties and call required one:

class Foo {
    public bar: string = null; // property has to be initialized
}

function getPropertyName<T>(TCreator: { new(): T; }, expression: Function): string {
    let obj = new TCreator();
    Object.keys(obj).map(k => { obj[k] = () => k; });
    return expression(obj)();
}

let result = getPropertyName(Foo, (o: Foo) => o.bar);
console.log(result); // Output: `bar`

The same approach but objects instead of classes is here

artemnih
  • 4,136
  • 2
  • 18
  • 28
1

I like lambda-based approach (but you should use keyof most of the time if suffice/possible):

type valueOf<T> = T[keyof T];
function nameOf<T, V extends T[keyof T]>(f: (x: T) => V): valueOf<{ [K in keyof T]: T[K] extends V ? K : never }>;
function nameOf(f: (x: any) => any): keyof any {
    var p = new Proxy({}, {
        get: (target, key) => key
    })
    return f(p);
}
// Usage:
nameOf((vm: TModel) => vm.prop)
SalientBrain
  • 2,431
  • 16
  • 18
0

After some debugging I did find an answer, but feel free to give a better answer if you have any. Code & explanation below...:

Since you pass an array of functions via propertySelectors: ((x: T) => any | string)[], you can strip out the body of each function. Then you strip out the return. and ; parts of each function, so you end up with only the property name. E.g.:

  1. function (v) { v.id; }
  2. after the first .slice() step this becomes v.id;
  3. after the second .slice() step this becomes id

Some warnings! This doesn't cover nested properties, and the performance of this might not be ideal as well. Hower for my usecase this was enough, but any ideas or improvements are welcome. For now I won't search any further - as it's not needed for my usecase.

The gist of the code is in here:

let properties: string[] = [];
    propertySelector.forEach(propertySelector => {
      const functionBody = propertySelector.toString();
      const expression = functionBody.slice(functionBody.indexOf('{') + 1, functionBody.lastIndexOf('}'));
      const propertyName = expression.slice(expression.indexOf('.') + 1, expression.lastIndexOf(';'));
      properties.push(propertyName.trim());
    });  

Implemented in an angular service it looks like this:

import { Injectable } from '@angular/core';
import { IPropertySelector } from '../../models/property-selector.model';

@Injectable()
export class ObjectService {     
    extractPropertyNames<T>(propertySelectors: IPropertySelector<T>[]): string[] {
        let propertyNames: string[] = [];

        propertySelectors.forEach(propertySelector => {
            const functionBody = propertySelector.toString();
            const expression = functionBody.slice(functionBody.indexOf('{') + 1, functionBody.lastIndexOf('}'));
            const propertyName = expression.slice(expression.indexOf('.') + 1, expression.lastIndexOf(';'));
            propertyNames.push(propertyName);
        });

        return propertyNames;
    }
}

And used like this in a method of a component where the service is injected:

  private searchFilter(model: Model, q: string, propertySelectors: IPropertySelector<Model>[]): boolean {
    if (q === '') return true;

    q = q.trim().toLowerCase();

    if (!this.cachedProperties) {
      this.cachedProperties = this.objectService.extractPropertyNames(propertySelectors);
    }

    for (let property of this.cachedProperties) {
      if (model[property].toString().toLowerCase().indexOf(q) >= 0) {
        return true;
      }
    }

    return false;
  }

Interface for ease of use

export interface IPropertySelector<T> {
    (x: T): any;
}
Yves Schelpe
  • 3,343
  • 4
  • 36
  • 69