26

Question/Answer - Update 2021

This questions was asked 6 years ago, and I had very little understanding of Typescript! I don't want to remove it because there are still some people reading this post.

If you want the type of a variable to be a property of another one, you can use keyof.

Example:

interface User {
    name: string;
    age: number;
}

const nameProperty: keyof User = 'name'; // ok
const ageProperty: keyof User = 'age'; // ok
const emailProperty: keyof User = 'email'; // not ok

If you want a method that takes a parameter which is a property of another parameter you can use generics to link both types together.

Example using generics + keyof:

const foo = <TObject extends object>(
    object: TObject,
    property: keyof TObject
) => {
    // You can use object[property] here
};

foo({ a: 1, b: 2 }, 'a'); // ok
foo({ a: 1, b: 2 }, 'b'); // ok
foo({ a: 1, b: 2 }, 'c'); // not ok

Example using generics + Record:

const foo = <TKey extends string>(
    object: Record<TKey, unknown>,
    property: TKey
) => {
    // You can use object[property] here
};

foo({ a: 1, b: 2 }, 'a'); // ok
foo({ a: 1, b: 2 }, 'b'); // ok
foo({ a: 1, b: 2 }, 'c'); // not ok

Don't use this question answers please! Typescript will automatically tell you that there is an error if you rename the property at some point.


Original question (2014)

Objective

I have an interface TypeScript :

interface IInterface{
    id: number;
    name: string;
}

I have some methods which take in entry the name of a property (string).

Ex :

var methodX = ( property: string, object: any ) => {
    // use object[property]
};

My problem is that when i call methodX, I have to write the property name in string.

Ex : methodX("name", objectX); where objectX implements IInterface

But this is BAD : If i rename a property (let's say i want to rename name to lastname) i will have to update manually all my code.

And I don't want this dependency.

As typescript interfaces have no JS implementations, I don't see how I could not use string.

I want to have something like : methodX(IInterface.name.propertytoString(), objectX);

I'm pretty new to JS, do you see an alternative ?

(Optional) More details : Why do I need to pass properties as parameter, and why I don't use a generic method ?

I use methods that link data :

linkData = <TA, TB>(
    inputList: TA[],
    inputId: string,
    inputPlace: string,
    outputList: TB[],
    outputId: string ) => {

    var mapDestinationItemId: any = {};
    var i: number;
    for ( i = 0; i < outputList.length; ++i ) {
        mapDestinationItemId[outputList[i][outputId]] = outputList[i];
    }

    var itemDestination, itemSource;
    for ( i = 0; i < inputList.length; ++i ) {
        itemDestination = inputList[i];
        itemSource = mapDestinationItemId[itemDestination[inputId]];
        if ( itemSource ) {
            itemDestination[inputPlace] = itemSource;
        }
    }
};

But TA and TB can have a lot of different ids. So i don't see how to make it more generic.

6 Answers 6

15

Update 2019: This answer is outdated, please look at the update added directly into the question.


basarat answer is a good idea, but it doesn't work with interfaces.

You can't write methodX(interfacePropertyToString(()=>interfaceX.porpertyname), objectX) because interfaceX is not an object.

Interfaces are abstractions and they are used only for TypeScript, they doesn't exist in Javascript.

But thanks to his answer i found out the solution : using a parameter in the method.

Finally we have :

    interfacePropertyToString = ( property: (object: any) => void ) => {
        var chaine = property.toString();
        var arr = chaine.match( /[\s\S]*{[\s\S]*\.([^\.; ]*)[ ;\n]*}/ );
        return arr[1];
    };

We have to use [\s\S] to be able to match on multilines because Typescript convert (object: Interface) => {object.code;} to a multiline function.

Now you can use it as you want :

        interfacePropertyToString(( o: Interface ) => { o.interfaceProperty});
        interfacePropertyToString( function ( o: Interface  ) { o.interfaceProperty});
Sign up to request clarification or add additional context in comments.

5 Comments

Great answer! Could you please explain a little about how TypeScript deals with interfaces defined by the user and what does it compile them into? Because I didn't find much about this. Thanks!
Also, can you think of any way to extract all properties of an interface this way? Thanks!
@radu-matei A TypeScript interface does not transpile into Javascript. It simply does not exist in Javascript.
Getting this error: Cannot read properties of null (reading '1')
Not sure where you guys got that regex, but based on the code and assuming a format like this: o => o.SomeProperty you'd need to update the regex like so [a-zA-Z]+\s=>\s[a-zA-Z]+\.([A-Za-z0-9]+) and then use return arr[2]. The regex basically puts a group around the prop name so that we can pull it out of arr
3

I've changed basarat code a little bit, so we can use it as generic:

const P = <T>( property: (object: T) => void ) => {
    const chaine = property.toString();
    const arr = chaine.match( /[\s\S]*{[\s\S]*\.([^\.; ]*)[ ;\n]*}/ );
    return arr[1];
};

And example usage:

console.log(P<MyInterface>(p => p.propertyName));

1 Comment

This should be the chosen answer!
3

For browsers that support the Proxy class:

function propToString<T>(obj?: T): T {
  return new Proxy({}, {
    get({}, prop) {
      return prop;
    }
  }) as T;
}

class Foo {
  bar: string;
  fooBar: string;
}

console.log(propToString<Foo>().bar, propToString(new Foo()).fooBar);
// Prints: bar fooBar

// Cache the values for improved performance:
const Foo_bar = propToString<Foo>().bar;

1 Comment

This is excellent. get should probably not take an empty object, and should instead take a param name. Code quality tools can flag the empty destructure as a bug. _ could be used to signify the parameter is unimportant. get(_, prop)....
2

You could write a function to parse the body of a function to find the name e.g.:

methodX(getName(()=>something.name), objectX)

Where getName will do a toString on the function body to get a string of the form "function(){return something.name}" and then parse it to get "name".

Note: however this has a tendency to break depending upon how you minify it.

Comments

0

Somewhat related problem - how to get/set a value to a property path. I wrote two classes for that:

export class PropertyPath {
    static paths = new Map<string, PropertyPath>()

    static get<T, P>(lambda: (prop:T) => P) : PropertyPath {
        const funcBody = lambda.toString();
        var ret : PropertyPath = this.paths[funcBody];
        if (!ret) {
            const matches = funcBody.match( /(?:return[\s]+)(?:\w+\.)((?:\.?\w+)+)/ ); //first prop ignores
            var path = matches[1];
            ret = new PropertyPath(path.split("."));
            this.paths[funcBody] = ret;
        }
        return ret;
    };

    path : Array<string>

    constructor(path : Array<string>) {
        this.path = path
    }

    getValue( context : any) {
        const me = this;
        var v : any;
        return this.path.reduce( (previous, current, i, path) => {
            try {
                return previous[current];
            }
            catch (e) {
                throw {
                    message : `Error getting value by path. Path: '${path.join(".")}'. Token: '${current}'(${i})`,
                    innerException: e
                };
            }
        }, context)
    }

    setValue( context : any, value : any) {
        const me = this;
        var v : any;
        this.path.reduce( (previous, current, i, path) => {
            try {
                if (i == path.length - 1) {
                    previous[current] = value
                }
                return previous[current];
            }
            catch (e) {
                throw {
                    message : `Error setting value by path. Path: '${path.join(".")}'. Token: '${current}'(${i}). Value: ${value}`,
                    innerException: e
                };
            }
        }, context)
    }

}

Example of usage:

var p = PropertyPath.get((data:Data) => data.person.middleName)
var v = p.getValue(data)
p.setValue(data, newValue)

Some sugar over it:

export class PropertyPathContexted {

    static get<T, P>(obj : T, lambda: (prop:T) => P) : PropertyPathContexted {
        return new PropertyPathContexted(obj, PropertyPath.get(lambda));
    };

    context: any
    propertyPath: PropertyPath

    constructor(context: any, propertyPath: PropertyPath) {
        this.context = context
        this.propertyPath = propertyPath
    }

    getValue = () => this.propertyPath.getValue(this.context)

    setValue = ( value : any) => {this.propertyPath.setValue(this.context, value) }

}

And usage:

var p = PropertyPathContexted.get(data, () => data.person.middleName)
var v = p.getValue()
p.setValue("lala")

I find the the latest quite convenient in two-way databinding in React:

var valueLink = function<T, P>( context: T, lambda: (prop:T) => P) {
    var p = PropertyPathContexted.get(context, lambda);
    return {
        value: p.getValue(),
        requestChange: (newValue) => {
            p.setValue(newValue);
        }
    }
};

render() {
   var data = getSomeData()
   //...
   return (
       //...
       <input name='person.surnames' placeholder='Surnames' valueLink={valueLink(data, () => data.person.surnames)}/>
       //...
   )
}

Comments

0

If you need to validate the strings you can create a new type based on keyof from the interface. If you have an object you can use keyof typeof object.

Example for language files:

localizationService.ts

import svSE from './languages/sv-SE';
import enUS from './languages/en-US';
import arSA from './languages/ar-SA';
import { ILanguageStrings } from './ILanguageStrings';

/*
If more languages are added this could be changed to:
    "sv-SE": svSE,
    "en-US": enUS,
    "ar-SA": arSA
*/

export const messages = {
    "sv": svSE,
    "en": enUS,
    "ar": arSA
};

//Identical types
export type IntlMessageID = keyof typeof messages.en;
export type IntlMessageID2 = keyof ILanguageStrings;

enter image description here

ILanguageStrings.ts

export interface ILanguageStrings {
    appName: string
    narration: string
    language: string
    "app.example-with-special-charactes": string
}

en-US.ts

import { ILanguageStrings } from '../ILanguageStrings';

const language: ILanguageStrings = {
    appName: "App Eng",
    narration: "Narration",
    language: "Language",
    "app.example-with-special-charactes": "Learn React."
}

export default language;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.