43

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.

Alexander Abakumov
  • 12,301
  • 14
  • 79
  • 125
shadeglare
  • 6,136
  • 5
  • 46
  • 54
  • 1
    There are currently some suggestions on github for helping with this (See [#1579](https://github.com/Microsoft/TypeScript/issues/1579), [#394](https://github.com/Microsoft/TypeScript/issues/394), and [#1003](https://github.com/Microsoft/TypeScript/issues/1003)). You could check out [this](http://stackoverflow.com/a/32542368/188246), but beware it might not work once the code is minified. – David Sherret Nov 05 '15 at 15:11
  • @DavidSherret your `this` solution is the only answer that I can come up with as well. Please post as answer – basarat Nov 05 '15 at 23:21

4 Answers4

64

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

const propertyOf = <TObj>(name: keyof TObj) => name;

or

const propertiesOf = <TObj>(_obj: (TObj | undefined) = undefined) => <T extends keyof TObj>(name: T): T => name;

or using Proxy

export const proxiedPropertiesOf = <TObj>(obj?: TObj) =>
  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(myObj);
myInterfaceProperties.myProperty;

This will give an error if myProperty is not a property of the type MyObj.

nzjoel
  • 996
  • 9
  • 16
  • 2
    This is great. here is a sample of how it can be added to a class https://gist.github.com/anonymous/5d5d041b4671480855070af478eb3fc2 – David Wilton Feb 15 '18 at 04:47
  • 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. – AL - Divine Jun 11 '20 at 14:23
32

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";
Community
  • 1
  • 1
David Sherret
  • 92,051
  • 24
  • 178
  • 169
  • for a => a.property I found that i needed to remove the '\}' from the regex – George Onofrei May 11 '17 at 10:47
  • 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. – panepeter Jan 15 '21 at 13:07
  • 1
    Don't use `ts-nameof`. It is deprecated by the author: https://github.com/dsherret/ts-nameof/issues/121 – Viacheslav Dobromyslov Dec 23 '21 at 19:15
2

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.

Alexander Abakumov
  • 12,301
  • 14
  • 79
  • 125
theapache64
  • 9,428
  • 7
  • 57
  • 87
0

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

James Bond
  • 1,357
  • 1
  • 12
  • 21