nameof(TypeScript);
How much am I abusing the system? Opinions & better solutions welcome! https://stackoverflow.com/a/44738872/1155847?stw=2
TypeScript is typed, duh. So whenever, wherever I can use types now in JavaScript (/TypeScript), I want to be able to! Then came the case of extracting properties/fieldnames of an object - typed! Why? Because I wanted to write code like this (see underlined in red):
Consider a model/object with properties "id", "firstName" and "lastName". When using "magic strings" you'd have to write code like this: `['id', 'firstName' 'lastName']` which is error prone, not rename friendly at all. But when writing typed code like this: `[p => p.id, p => p.firstName, p => p.lastName]` you'll get awesome intellisense support, type safety and it's rename friendly! The downside is a bit of processing ofcourse (see the example code below) - but I'm sure more clever minds can optimize this - and there's always caching if needed!
So, with the concept and idea in mind, and with me having a C# background.. My first attempt was to use nameof(), but TypeScript says no - so I had to find another solution to avoid using magic strings. I remembered TypeScript does support generics, so passing a lambda would surely be the solution - vague memories of C# propertySelectors came to mind, this has to be it... Something lambda, something Expression<Func<... But, oh noes, unfortunately, (Member)Expression isn't implemented in js or ts ofcourse...
But in the end it lead me to the same kind of idea.
Since you pass an array of functions via propertySelectors: ((x: T) => any | string)[] (or a more convenient interface IPropertySelector - see code example below), 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.:
- function (v) { v.id; }
- after the first .slice() step this becomes v.id;
- 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;
}
}
export interface IPropertySelector<T> {
(x: T): any;
}
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;
}
You could also use 'keyof' in typescript: https://gist.github.com/jansabbe/a5fffeef3a5e75ef196524bbb2b8fc5d