antoinestv
antoinestv

Reputation: 3306

Typescript interface property to string

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.

Upvotes: 26

Views: 41341

Answers (6)

Ogglas
Ogglas

Reputation: 69948

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;

Upvotes: 0

Jonny Cook
Jonny Cook

Reputation: 684

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;

Upvotes: 3

antoinestv
antoinestv

Reputation: 3306

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});

Upvotes: 15

BrightShadow
BrightShadow

Reputation: 463

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

Upvotes: 3

plinyar
plinyar

Reputation: 323

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)}/>
       //...
   )
}

Upvotes: 0

basarat
basarat

Reputation: 276239

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.

Upvotes: 2

Related Questions