Search code examples
c#typescriptreflectionreflect-metadatatypescript-decorator

Pass object and attribute at once for reflect-metadata in TypeScript like in C#


In C# we've DataAnnotation to add meta attributes on properties. I need this functionality in TypeScript for a ldap model class. Decorators should set the LDAP attribute which is internally used in the LDAP directory

export class LdapUser {
    @ldapAttribute('sn')
    sureName: string;
}

export function ldapAttribute(attributeName: string): (target: any, propertyKey: string) => void {
    return function (target: any, propertyKey: string) {
        Reflect.defineMetadata("ldapAttribute", attributeName, target, propertyKey);
    }
}

But to fetch the ldapAttributedecorator value I need to pass object and attribute name as raw strings like this:

let user = new LdapUser();
let sureNameAttribute = Reflect.getMetadata("ldapAttribute", user, "sureName"); // sn

It works, but that seems bad practice, since it would result in a runtime instead of compiler error when sureNameattribute is renamed in LdapUserwithout applying this to the Reflect.getMetadata()call. And also intellisense is missing. So I'm searching for a solution like this:

let sureNameAttribute = Reflect.getMetadata("ldapAttribute", user.sureName);

Problem here is that I need some sort of reflection to divide user.SureNamein the attribute name (here sureName) and class object (here user). I already did something like this in C# using reflection, but have no clue how to do this in TS.

Workaround

It's not as good as it would be using Reflection in C#, but better than only use plain strings:

export function getLdapAttribute<T>(instance: T, attributeName: keyof T) : string {
    let value : string = Reflect.getMetadata("ldapAttribute", instance, attributeName);
    return value;
}

usage

let attributeValue = getLdapAttribute(user, "sureName"); // cn

Sadly we have no intellisense here. But at least we get an compiler error if the attribute name doesnt exist.


Solution

  • We use next approach to solve this problem:

    export type FieldSpec<TModel, TResult> = ((model?: TModel) => TResult) | string;
    
    export function getFieldName<TModel, TResult>(fieldSpec: FieldSpec<TModel, TResult>): string {
        if (typeof (fieldSpec) == "string" || !fieldSpec) {
            return fieldSpec as string;
        } else {
            var fullFunc = fieldSpec.toString();
            var parts = fullFunc.split(/[.;}]/).filter(x => x.trim().length > 0);
            return parts[parts.length - 1].trim();
        }
    }
    

    With these helpers you can write:

    export function getLdapAttribute<T>(instance: T, attributeName: FieldSpec<T,any>) : string {
        let value : string = Reflect.getMetadata("ldapAttribute", instance, getFieldName(attributeName));
        return value;
    }
    
    let user = new LdapUser();
    let sureNameAttribute = getLdapAttribute(user, () => user.sureName);