shadeglare
shadeglare

Reputation: 7536

Safe way to extract property names

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.

Upvotes: 60

Views: 91713

Answers (4)

nzjoel
nzjoel

Reputation: 1206

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;

Upvotes: 84

James Bond
James Bond

Reputation: 2986

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

Upvotes: 2

theapache64
theapache64

Reputation: 11754

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.

Upvotes: 3

David Sherret
David Sherret

Reputation: 106650

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";

Upvotes: 35

Related Questions