52

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
  • 13,617
  • 16
  • 88
  • 129
shadeglare
  • 7,006
  • 7
  • 47
  • 59
  • 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

4 Answers4

75

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;
nzjoel
  • 1,106
  • 9
  • 17
  • 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
  • 1
    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 - Lil Hunk Jun 11 '20 at 14:23
  • 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? – OfirD Jul 14 '22 at 14:52
  • A few years later, `const propertyOf = (name: keyof TObj) => name;` doesn't compile. You need to add a comma, `const propertyOf = (name: keyof TObj) => name;`. – MEMark Aug 25 '22 at 06:31
  • 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://stackoverflow.com/q/73742217/274677 – Marcus Junius Brutus Sep 16 '22 at 09:09
  • If you use the Proxy style - then `propertiesOf().myProperty` will return a strongly typed string literal. – nzjoel Sep 20 '22 at 01:45
  • @MEMark that compiles fine for me - not sure why you would need to put the trailing comma there? – nzjoel Sep 20 '22 at 01:46
  • 1
    @nzjoel It's because the compiler interprets it as JSX (React). Here is a playground showing the compile error https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgJxHApgqBPA8gMwOoCWUAFgMIgC2FAhjALwwA8AKlgEYBWANAHwAUYahRQAuGAGsUGEDhitOASno8Yg4QG4AUJtCRYiZGky5CJEAFco5KrQYt2HfmtESpMuQ6V0VzrUA. – MEMark Sep 20 '22 at 07:05
  • 1
    @MEMark I see. I have updated answer to using basic function declarations rather than arrow functions which should resolve the issue. – nzjoel Sep 22 '22 at 04:38
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";
Community
  • 1
  • 1
David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • 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
  • 5
    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
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.

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
theapache64
  • 10,926
  • 9
  • 65
  • 108
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
  • 2,229
  • 1
  • 15
  • 26