Passing public field names with typed expressions in TypeScript

I've been exploring ways to pass an array of property names (or field names) for a specific object without resorting to using what are often referred to as "magic strings" - as they can easily lead to typos! I'm essentially searching for something similar to csharp's "Expression<>".

For example, using magic strings:

searchFilter(model, 'searchParameter', ['id', 'name'])

Alternatively, in a typed manner, or how I envision calling the function:

searchFilter(model, 'searchParameter', [m => m.id, m => m.name])

Here's a sample representation of this function:

Using magic strings: (or my attempted approach with typing)

private searchFilter(mode: Model, q: string, properties: string[]): boolean {
   if (q === '') return true;

   q = q.trim().toLowerCase();

   for (let property of properties) {
     if (vacature[property.toString()].toString().toLowerCase().indexOf(q) >= 0) {
       return true;
     }
  }

  return false;
}

Typed: (Or how I initially tried to implement it with typing, however, this simply returns functions. To effectively extract the called property and obtain its name, I would need a relevant 'function expression' akin to C#.)

private searchFilter(mode: Model, q: string, propertySelector: ((x: Model) => any | string)[]): boolean {
   if (q === '') return true;

   q = q.trim().toLowerCase();

   for (let property of propertySelector) {
     if (vacature[property.toString()].toString().toLowerCase().indexOf(q) >= 0) {
       return true;
     }
  }

  return false;
 }

Answer №1

If you're struggling to remove the string due to the absence of a nameof property in typescript, don't worry as it doesn't exist yet.

One workaround is to type something as the key of another type, just like this:

interface Model {
    a: string,
    b: number
}

function searchFilter(model: Model, q: keyof Model) { }

With this approach, you get the following functionality:

searchFilter(null, 'a') // works
searchFilter(null, 'b') // works
searchFilter(null, 'c') // error c is not a property of Model

You can also type an array of properties of a type in a similar manner:

function searchArray(model: Model, q: string, properties: Array<keyof Model>) { }

searchArray(null, 'blabla', ['a', 'b'])

Answer №2

The functionality of Nameof is not available by default, but a third-party library has been used to replicate it.

To achieve the capabilities of nameof, you can utilize a third-party library found at https://www.npmjs.com/package/ts-nameof. The source code for this library can be viewed here: https://github.com/dsherret/ts-nameof

This library offers various options depending on the level of object naming you require, such as the name of a variable itself, the name of a method, or even the name of a method along with its containing class (information taken from the library documentation).

Below demonstrates the transpiled JavaScript output on the left side and its equivalent TypeScript representation on the right side.

console.log("console");             // console.log(nameof(console));
console.log("log");                 // console.log(nameof(console.log));
console.log("console.log");         // console.log(nameof.full(console.log));
console.log("alert.length");        // console.log(nameof.full(window.alert.length, 1));
console.log("length");              // console.log(nameof.full(window.alert.length, 2));
console.log("length");              // console.log(nameof.full(window.alert.length, -1));
console.log("alert.length");        // console.log(nameof.full(window.alert.length, -2));
console.log("window.alert.length"); // console.log(nameof.full(window.alert.length, -3));

"MyInterface";                      // nameof<MyInterface>();
console.log("Array");               // console.log(nameof<Array<MyInterface>>());
"MyInnerInterface";                 // nameof<MyNamespace.MyInnerInterface>();
"MyNamespace.MyInnerInterface";     // nameof.full<MyNamespace.MyInnerInterface>();
"MyInnerInterface";                 // nameof.full<MyNamespace.MyInnerInterface>(1);
"Array";                            // nameof.full<Array<MyInterface>>();
"prop";                             // nameof<MyInterface>(o => o.prop);

These strings are replaced during transpiration, so there should be no impact on runtime performance.

Answer №3

Creating closure methods with the same names as properties and executing the required one is a common practice:

class MyClass {
    public myProperty: string = null; // initialize property
}

function getPropertyName<T>(TCreator: { new(): T; }, expression: Function): string {
    let obj = new TCreator();
    Object.keys(obj).map(key => { obj[key] = () => key; });
    return expression(obj)();
}

let result = getPropertyName(MyClass, (obj: MyClass) => obj.myProperty);
console.log(result); // Output will be `myProperty`

A similar technique can also be used for objects, as explained here

Answer №4

I appreciate the lambda-based approach, although I suggest utilizing keyof in most cases when possible:

type valueOf<T> = T[keyof T];
function keyOfValue<T, V extends T[keyof T]>(func: (input: T) => V): valueOf<{ [K in keyof T]: T[K] extends V ? K : never }>;
function keyOfValue(func: (input: any) => any): keyof any {
    var proxy = new Proxy({}, {
        get: (target, property) => property
    })
    return func(proxy);
}
// Example of usage:
keyOfValue((model: MyModel) => model.property)

Answer №5

After troubleshooting, I managed to find a solution. However, if you have a better answer, feel free to share it. Below is the code and explanation:

By passing an array of functions through

propertySelectors: ((x: T) => any | string)[]
, you can extract the body of each function. Then remove the return and ; parts from each function, leaving only the property name. For example:

  1. function (v) { v.id; }
  2. after the first .slice() step this becomes v.id;
  3. after the second .slice() step this becomes id

Some warnings! This method does not account for nested properties and may not be optimal in terms of performance. However, it sufficed for my use case. Any suggestions or enhancements are welcomed. At the moment, I won't delve deeper as it's unnecessary for my current needs.

The main code snippet is provided below:

let properties: string[] = [];
    propertySelector.forEach(propertySelector => {
      const functionBody = propertySelector.toString();
      const expression = functionBody.slice(functionBody.indexOf('{') + 1, functionBody.lastIndexOf('}'));
      const propertyName = expression.slice(expression.indexOf('.') + 1, expression.lastIndexOf(';'));
      properties.push(propertyName.trim());
    });  

When implemented in an Angular service, it appears as follows:

import { Injectable } from '@angular/core';
import { IPropertySelector } from '../../models/property-selector.model';

@Injectable()
export class ObjectService {     
    extractPropertyNames<T>(propertySelectors: IPropertySelector<T>[]): string[] {
        let propertyNames: string[] = [];

        propertySelectors.forEach(propertySelector => {
            const functionBody = propertySelector.toString();
            const expression = functionBody.slice(functionBody.indexOf('{') + 1, functionBody.lastIndexOf('}'));
            const propertyName = expression.slice(expression.indexOf('.') + 1, expression.lastIndexOf(';'));
            propertyNames.push(propertyName);
        });

        return propertyNames;
    }
}

Usage within a component method where the service is injected:

  private searchFilter(model: Model, q: string, propertySelectors: IPropertySelector<Model>[]): boolean {
    if (q === '') return true;

    q = q.trim().toLowerCase();

    if (!this.cachedProperties) {
      this.cachedProperties = this.objectService.extractPropertyNames(propertySelectors);
    }

    for (let property of this.cachedProperties) {
      if (model[property].toString().toLowerCase().indexOf(q) >= 0) {
        return true;
      }
    }

    return false;
  }

Interface for convenience:

export interface IPropertySelector<T> {
    (x: T): any;
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the interaction between custom HTML tags and cloning a template in web development?

I'm feeling stuck with this particular test case. In the ending part of the html code, I have: <template id="test"> <test-tag class="test-id"></test-tag> </template> Within the script: class TestTag ext ...

JSX tags without any inner content should be self-closed

After successfully running this code, I encountered an issue when committing it to git. The error message 'ERROR: src/layouts/index.tsx:25:9 - JSX elements with no children must be self-closing' appeared. I attempted to resolve the error by addi ...

Upon initialization, navigate to the specified location in the route by scrolling

My page has various components stacked one after the other, such as: <about></about> <contact></contact> I am utilizing the ng2-page-scroll to smoothly scroll to a particular section when a navigation link is clicked. However, I a ...

Utilizing TypeScript to perform typing operations on subsets of unions

A TypeScript library is being developed by me for algebraic data types (or other names they may go by), and I am facing challenges with the more complex typing aspects. The functionality of the algebraic data types is as follows: // Creating ADT instatiat ...

The attribute on the label fails to trigger any action on the linked input upon clicking, due to the binding of the id and for

My component is designed to display a checkbox and label, with inputs for id, name, label, and value. Here's the code: <div class="checkbox col-xs-12" *ngIf="id && name && label && value"> <input type="checkbox" ...

Converting a Typescript project into a Node package: A step-by-step guide

I'm currently dealing with an older typescript project that has numerous functions and interfaces spread out across multiple files. Other packages that depend on these exports are directly linked to the file in the directory. My goal is to transition ...

The filename is distinct from the file already included solely by the difference in capitalization. Material UI

I have recently set up a Typescript React project and incorporated Material UI packages. However, I encountered an error in VS Code when trying to import these packages - although the application still functions properly. The error message reads: File na ...

The type 'any' cannot be assigned to the parameter type 'never' in this argument.ts

I received a warning: Argument of type 'any' is not assignable to parameter of type 'never'.ts(2345) Object is of type 'unknown'.ts(2571) Here is my request body: {"A":[{"filter":[{"a":"a"}]},{"group":[{"a":"a"}]}],"B":[{"f ...

Combining interfaces in Typescript

UPDATE i stumbled upon this solution type Merge<T, U> = { [K in keyof T | keyof U]: K extends keyof U? U[K]: K extends keyof T? T[K]: never; }; is there an equivalent in typescript using the spread operator? type Merge<T, U> = ...[K in ke ...

Make sure that the Chai assertion does not result in any errors

One of my functions involves retrieving file content. export function getFileContent(path: string): any { const content = readFileSync(path); return JSON.parse(content.toString()); } If I need to verify that calling getFileContent(meteFile) result ...

Accessing one controller from another module in AngularJS using TypeScript is a common challenge that many developers face. In this article, we

// inside engagement.component.ts: class EngagementMembersController { alphabetic: Array<string> = 'abcdefghijklmnopqrstuvwxyz'.split(''); constructor() {} export const EngagementSetupMember: IComponentOptions ...

Organizing Angular models and interfaces

The Angular styleguide provides best practices for using classes and interfaces in applications, but it does not offer guidance on organizing interfaces and model classes. One common question that arises is: what are the best practices for organizing file ...

Parameters for constructing classes in TypeScript

I've been exploring different coding styles in TypeScript recently. When it comes to initializing an object from a class, what are the advantages and disadvantages of these two code styles in TypeScript? class Class3 { // members private rea ...

An error has occurred in Angular: No routes were found that match the URL segment 'null'

I've recently developed a simple Angular page that extracts an ID (a guid) from the URL and uses it to make an API call. While I have successfully implemented similar pages in the past without any issues, this particular one is presenting challenges w ...

Consolidate functions into a unified method through the implementation of TypeScript Generics

How can I refactor this code using TypeScript generics? I have three methods here, and I would like to consolidate them into a single method that accepts type T in order to avoid redundant code. private initializeComponentIframe<T>(componentType: ...

Using NextJS: Issue with updating Value in useState

In my current project, I am attempting to display a string that changes when a button is pressed in my NextJs application. Here's the code snippet I am working with: 'use client' import { useState } from 'react' export default fu ...

Optimize Next.js 10 TypeScript Project by Caching MongoDb Connection in API Routes

I am currently in the process of transitioning next.js/examples/with-mongodb/util/mongodb.js to TypeScript so I can efficiently cache and reuse my connections to MongoDB within a TypeScript based next.js project. However, I am encountering a TypeScript err ...

Typescript - type assertion does not throw an error for an invalid value

When assigning a boolean for the key, I have to use a type assertion for the variable 'day' to avoid any errors. I don't simply do const day: Day = 2 because the value I receive is a string (dynamic value), and a type assertion is necessary ...

Combining component attributes with a mixin in Vue 2 using TypeScript

In my Vue + TypeScript project, we are utilizing Vue class components. Recently, I moved one of the component's methods to a separate mixin that relies on the component's properties. To address TypeScript errors regarding missing properties in th ...

Can the tooltip placement be adjusted in ng-bootstrap when it reaches a specific y-axis point?

Currently, I am facing an issue with my tooltip appearing under the header nav-bar instead of flipping to another placement like 'left-bottom' when it reaches the header. Is there a way to manually set boundaries for tooltips in ng-bootstrap? Unl ...