Safe way to extract property names
Asked Answered
R

4

57

I'm looking for a way to get an object property name with typechecking that allows to catch possible regressions after refactoring.

Here's an example: the component where I have to pass the property names as strings and it will be broken if I'll try to change the property names in the model.

interface User {
   name: string;
   email: string;
}

class View extends React.Component<any, User> {

   constructor() {
      super();
      this.state = { name: "name", email: "email" };
   }

   private onChange = (e: React.FormEvent) => {
      let target = e.target as HTMLInputElement;
      this.state[target.id] = target.value;
      this.setState(this.state);
   }

   public render() {
      return (
         <form>
            <input
               id={"name"}
               value={this.state.name}
               onChange={this.onChange}/>
            <input
               id={"email"}
               value={this.state.email}
               onChange={this.onChange}/>
            <input type="submit" value="Send" />
         </form>
      );
   }
}

I'd appreciate if there's any nice solution to solve this issue.

Rational answered 5/11, 2015 at 14:48 Comment(1)
There are currently some suggestions on github for helping with this (See #1579, #394, and #1003). You could check out this, but beware it might not work once the code is minified.Unkind
W
80

In TS 2.1 the keyof keyword was introduced which made this possible:

function propertyOf<TObj>(name: keyof TObj) {
    return name;
}

or

function propertiesOf<TObj>(_obj: (TObj | undefined) = undefined) {
    return function result<T extends keyof TObj>(name: T) {
        return name;
    }
}

or using Proxy

export function proxiedPropertiesOf<TObj>(obj?: TObj) {
    return new Proxy({}, {
        get: (_, prop) => prop,
        set: () => {
        throw Error('Set not supported');
        },
    }) as {
        [P in keyof TObj]?: P;
    };
}

These can then be used like this:

propertyOf<MyInterface>("myProperty");

or

const myInterfaceProperties = propertiesOf<MyInterface>();
myInterfaceProperties("myProperty");

or

const myInterfaceProperties = propertiesOf(myObj);
myInterfaceProperties("myProperty");

or

const myInterfaceProperties = proxiedPropertiesOf<MyInterface>();
myInterfaceProperties.myProperty;

or

const myInterfaceProperties = proxiedPropertiesOf(myObj);
myInterfaceProperties.myProperty;
Worthwhile answered 28/2, 2017 at 19:8 Comment(9)
This is great. here is a sample of how it can be added to a class gist.github.com/anonymous/5d5d041b4671480855070af478eb3fc2Predetermine
Yes, this raises compile time error! Which is wonderful. I had to remove this from the interface though: [key: string]: any; because that made all properties/strings valid.Anny
AL-Divine's comment is on point, because a common use case for grabbing a property name is to use it dynamically to access the property value (which is only possible when using a key index). What can be done in such a case?Aracelis
A few years later, const propertyOf = <TObj>(name: keyof TObj) => name; doesn't compile. You need to add a comma, const propertyOf = <TObj,>(name: keyof TObj) => name;.Mickeymicki
This is wonderful, but the property extracted in this manner is typed simply as a loose string, not as a string literal. I posted a question on whether this can be done in a yet more rigorous way: https://mcmap.net/q/752997/-extract-property-names-in-a-strongly-typed-way/274677Gaelan
If you use the Proxy style - then propertiesOf<MyType>().myProperty will return a strongly typed string literal.Worthwhile
@Mickeymicki that compiles fine for me - not sure why you would need to put the trailing comma there?Worthwhile
@Worthwhile It's because the compiler interprets it as JSX (React). Here is a playground showing the compile error typescriptlang.org/play?#code/….Mickeymicki
@Mickeymicki I see. I have updated answer to using basic function declarations rather than arrow functions which should resolve the issue.Worthwhile
U
34

Right now there's not really a great way of doing this, but there are currently some open suggestions on github (See #1579, #394, and #1003).

What you can do, is what's shown in this answer—wrap referencing the property in a function, convert the function to a string, then extract the property name out of the string.

Here's a function to do that:

function getPropertyName(propertyFunction: Function) {
    return /\.([^\.;]+);?\s*\}$/.exec(propertyFunction.toString())[1];
}

Then use it like so:

// nameProperty will hold "name"
const nameProperty = getPropertyName(() => this.state.name);

This might not work depending on how the code is minified so just watch out for that.

Update

It's safer to do this at compile time. I wrote ts-nameof so this is possible:

nameof<User>(s => s.name);

Compiles to:

"name";
Unkind answered 5/11, 2015 at 23:31 Comment(3)
for a => a.property I found that i needed to remove the '\}' from the regexChiclayo
I'd very much suggest you make a separate answer for your very awesome ts-nameof package. It gets somewhat lost here in between the hacky function-to-string solution. Or just remove that part? Either way, your post shouldn't start the way it does, because there now is a great way of doing this.Circosta
Don't use ts-nameof. It is deprecated by the author: github.com/dsherret/ts-nameof/issues/121Ecclesiasticism
E
3

This is specifically for React/React-Native developers.

To safely get property-name, I use the below class:

export class BaseComponent<P = {}, S = {}> extends Component<P, S> {
  protected getPropName = (name: keyof P) => name;
  protected getStateName = (name: keyof S) => name;
}

And replaced extends React.Component<PropTypes> with extends BaseComponnent<PropTypes,

Now, with in the Component you can call, this.getPropName('yourPropName') to get the property name.

Engadine answered 27/6, 2018 at 7:46 Comment(0)
C
2

You can extract property name as string using keyof and Pick:

interface Test {
  id: number,
  title: string,
}

type TitleName = keyof Pick<Test, "title">;
     //^? type TitleName = "title"

const okTitle: TitleName = "title";
const wrongTitle : TitleName = "wrong";
     // Error: Type '"wrong"' is not assignable to type '"title"'

Playground

Cwm answered 3/6, 2022 at 23:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.