9

I would like to strongly type property names

myMethod(model => model.userId);

public myMethod(model: () => any) {
    //Must print "userId"
}

I already know this won't work because JavaScript will evaluate userId.

It's easily doable in C#:

Get string property name from expression

Retrieving Property name from lambda expression

Is it possible to do it in TypeScript/JavaScript?

olivierr91
  • 1,243
  • 3
  • 13
  • 29
  • 2
    Why not just pass the name in ? – Jonas Wilms Jun 20 '18 at 13:54
  • Instead of `() => any`, try `() => User` with `interface User { userId: number }`. Then when you do `model()` you will get a user. So you can directly do `model().userId` – Rajesh Jun 20 '18 at 13:58
  • @JonasW. Because it would not provide compile type checking of the property name. To put my question in context, this is for model validation and the method shows to the user which form field associated with the property of the model has an error. I already pass the name as a string, that's the thing I wanna get away from because it is too much error prone when renaming fields, etc. – olivierr91 Jun 20 '18 at 15:42
  • @Rajesh The method does not know in advance the name of the field, the caller must provide the name of the field. – olivierr91 Jun 20 '18 at 15:45
  • 1
    @sixstorm1 you can do this in a typesafe way! Just have a look at my answer. – Jonas Wilms Jun 20 '18 at 15:56

2 Answers2

9

Unlike in C#, one can dynamically access properties by their name in JavaScript (and thus also Typescript), so you could just pass the name as a string to the function and use bracket notation to access the property:

myMethod(model, "userId")

Now the cool thing about typescript that this string can actually be typesafe:

function myMethod<T, K extends keyof T>(model: T, key: K) {
  const value = model[key];
  //...
 }

Read on


If you really want to do something similar like you did in C# (don't!!) Just do this:

function myMethod(model: () => any) {
   const key = model.toString().split(".")[1];
   //...
}
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • Almost there, only problem is that model needs to be instantiated. What if my model is null at runtime? I tried `myMethod(typeof(MyModel), "userId")` but it does not work. I need to be able to retrieve the property name even if model is null. But best answer so far though. – olivierr91 Jun 20 '18 at 16:30
  • Even though you don't like it, I prefer option 2, it's cleaner code on the caller and does not rely on the instance of model: `myMethod(m => m.userId)` – olivierr91 Jun 20 '18 at 16:35
  • I am trying have an equivalent of Expr> of C# in TypeScript . Plenty of usages of this in C#, LINQ for example, also see links to posts I made. My specific usage is for model validation, the method shows to the user which form field associated with the property of the model has an error. Currently I pass the name of the model property as a string, I wanna get away from this practice because it is too much error prone (when developer rename fields for example), hard-coded strings are frequently forgotten. – olivierr91 Jun 20 '18 at 16:40
  • @sixtstorm1 wrong! passing a property as a string is *not* error prone in typescript. – Jonas Wilms Jun 20 '18 at 16:44
  • My current way of doing it IS NOT type-checked, so yes it is error prone. Please read my comment carefully. – olivierr91 Jun 20 '18 at 17:05
  • Your first answer provides type-checking, but depends on the runtime instance of model and will cause problem at runtime if model is null. Your second answer provides type-checking while not depending of the value of model at runtime. Allows me to do it very cleanly: `myMethod(m => m.userId)` This has been marked as the answer. – olivierr91 Jun 20 '18 at 17:09
  • You misunderstood, I don't want to do runtime typechecking. I said your first option will throw an exception at runtime if the model is null. The second option does not. That's why second option is better. – olivierr91 Jun 20 '18 at 23:15
8

If I understand what you're asking, you'd like to inspect a property-retrieving arrow function, and return the name of the property it's returning? That sounds like a Bad Idea for a weakly/dynamically typed language like JavaScript, and something any sane person should run screaming from. Still, assuming I were either insane or being coerced, here's how I would go about trying to doing it in TypeScript 2.9 compiled to ES2015:

type ValueOf<T> = T[keyof T];
function evilMagic<T, V extends T[keyof T]>(
  f: (x: T)=>V
): ValueOf<{[K in keyof T]: T[K] extends V ? K : never}>;
function evilMagic(f:(x: any)=>any): keyof any {
  var p = new Proxy({}, {
    get(target, prop) { return prop }
  })
  return f(p);
}

The function evilMagic takes a property-getting function and tries to return the name of the property it returns. The type of the output is hard to explain, but basically it will be some subset of keyof T where T is the argument the property-getting function expects. The implementation of the function uses a Proxy object p which acts like some object whose values are always the same as its keys. That is, p.foo is "foo", and p.bar is "bar", and p[10] is "10". Call the property-getter on p and, abracadabra, you have the name of the property.

Here's an example of its use:

interface Person {
  name: string;
  age: number;
  numberOfLimbs: number;      
}
const m = evilMagic((x: Person) => x.age); // typed as "age"|"numberOfLimbs";
console.log(m); // "age"

At compile time, TypeScript can only tell that m is one of "age" or "numberOfLimbs", because it only sees that the callback function returns a number. At runtime, you get "age" as you expect.

This whole thing scares me, though, and I feel unclean writing it; a function that expects a value of some type may do all kinds of crazy things when handed that proxy. I'd hate to think of code like that ending up in anything meant for production, although it could be a useful debugging tool. Please tell me you'd only use it for a debugging tool!

Anyway, hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Nice one. But I would clean up the tyedefinitions, thats horrible. – Jonas Wilms Jun 20 '18 at 15:59
  • My question is for TypeScript, which is a strongly-typed language, hence my desire of strongly typed access to properties of a class. There are plenty of acceptable and useful usages for what I am asking, see links to my post above, LINQ, etc. Thanks for the answer though. – olivierr91 Jun 20 '18 at 16:12
  • I think this is the most elegant solution. I use this for my date formats when I sanitize data for other components in my application. I have a decorator that scans the object coming in and lambda picks what fields has to be sanitized. Thank you for the solution! – kkdeveloper7 Jun 10 '21 at 13:45