Guide on creating a TypeScript function that extracts specific properties from an object using a filter criteria

Look at the code snippet provided below.

interface IEmployee {
    ID: string
    Employee: string
    Phone: number
    Town: string
    Email: string
    Name: string
}

// Retrieve all properties of type IEmployee 
let allProps = findSpecificItem<IEmployee>("ID = 1234234")

// Select only specific properties like Email and Phone from IEmployee  
let picked = findSpecificItem<IEmployee>("ID = 1234234", ["Email", "Phone"])

How would you define the function findSpecificItem? Is it possible to maintain compatibility with the existing calling code?

Possible approaches

Using arrays (Not recommended, does not work)

The following implementation is not functional as typeof fields[number] will result in Array<keyof T>

function findSpecificItem<T>(filter: string, fields: Array<keyof T> =[]): Pick<T,typeof fields[number]> {
    return db.select(filter, fields)
}

Introducing a second generic parameter (Effective approach)

This method works but might be less straightforward to understand

function select<T, K extends Partial<T> = T>(filter: string, fields = Array<keyof K>): K {
    return db.select(filter, fields)
}

const props = ["Email", "Phone"] as const
type propsType = Pick<IEmployee, typeof props[number]>

let allProps = select<IEmployee>("Employee: 1234234")
let picked = select<IEmployee, propsType>("Employee: 1234234")

Answer №1

In order to achieve functionality, it is necessary for findOne() to have generics in two type parameters: T, which corresponds to the base object type being found, and K, which corresponds to the union of keys from T that you want to select. The ideal implementation would look like this:

declare const findOne: <T, K extends keyof T>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

However, there's a drawback. When calling findOne(), you want to manually specify T, while also expecting the compiler to infer K from the keys argument without manual specification. Unfortunately, TypeScript does not support partial type argument inference. You either have to specify all type arguments or allow the compiler to infer all of them. (Even setting a default type argument for K doesn't enable partial inference; the compiler still won't infer it, but will fall back to the default.)


An ongoing feature request exists at microsoft/TypeScript#26242 for partial type argument inference. It might be possible one day to write something like this:

// THIS IS NOT VALID TYPESCRIPT, PLEASE DON'T TRY IT
declare const findOne: <T, K extends keyof T = infer>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

Until this feature is implemented, workarounds are required.

A preferred workaround involves currying the function, splitting the two type parameter function into one type parameter returning another function with a different type parameter, as shown here:

declare const findOne: <T>() => <K extends keyof T>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

This allows you to manually specify <T> when calling findOne, and the returned function will infer K:

let allProps: IEmployee = findOne<IEmployee>()("ID = 1234234")
let picked = findOne<IEmployee>()("ID = 1234234", ["Email", "Phone"])
// let picked: { Phone: number; Email: string; }

Although the extra function call may seem odd, it is effective! Once you start using curried functions in this manner, storing intermediate results for reuse becomes easier:

const findOneIEmployee = findOne<IEmployee>();
allProps = findOneIEmployee("ID = 1234234");
picked = findOneIEmployee("ID = 1234234", ["Email", "Phone"]);

Playground link to code

Answer №2

Not entirely sure if this is what you're looking for, but here's a variation of findOne that might help.

interface Cat {
  name: string
  age: number
  breed: string
  id: number
}

const cats: Cat[] = [
  {
    id: 1,
    name: 'Whiskers',
    age: 2,
    breed: 'Siamese'
  },
  {
    id: 2,
    name: 'Mittens',
    age: 5,
    breed: 'Persian'
  },
  {
    id: 3,
    name: 'Felix',
    age: 7,
    breed: 'Maine Coon'
  }
]

function findUnique<DataType, KeyType extends keyof DataType>(
  filter: [KeyType, keyof KeyType],
  items: DataType[],
  keys: KeyType[]
): Partial<DataType> | undefined {
  for (const item of items) {
    if (item[filter[0]] === filter[1]) {
      return {
        ...keys.reduce((acc, key) => {
          return {
            ...acc,
            [key]: item[key]
          }
        }, {})
      }
    }
  }
}

const cat = findUnique(['age', 7], cats, ['id', 'age'])

returns {
   id: 3, age: 7
}

To clarify things further, we can use an object for the filter:

function findUnique<DataType, KeyType extends keyof DataType>(
  filter: {
    key: KeyType
    value: keyof KeyType
  },
  items: DataType[],
  keys: KeyType[]
): Partial<DataType> | undefined {
  for (const item of items) {
    if (item[filter.key] === filter.value) {
      return {
        ...keys.reduce((acc, key) => {
          return {
            ...acc,
            [key]: item[key]
          }
        }, {})
      }
    }
  }
}

const cat = findUnique(
  {
    key: 'id',
    value: 1
  },
  cats,
  ['id', 'age']
)

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

Is it possible to release a typescript package without including the ts files in the

I have a Typescript project that needs to be published. All generated JS files are stored in a directory named build, and declaration files in another directory called declaration. I do not want the .ts files to be included in the published version. Can an ...

What is the proper way to utilize RxJS to append a new property to every object within an array that is returned as an Observable?

I'm not very familiar with RxJS and I have a question. In an Angular service class, there is a method that retrieves data from Firebase Firestore database: async getAllEmployees() { return <Observable<User[]>> this.firestore.collectio ...

What is causing ESLint to point out the issue with the @inheritdoc tag?

My code in ESLint is throwing an error about a missing JSDoc return declaration, even though I have included an @inheritdoc tag there: https://i.sstatic.net/QGxQh.png Here is the section from the interface I am inheriting from: export interface L2BlockSo ...

Encountering an infinite loop issue with React's useEffect and context: How to solve the "maximum update depth exceeded" console error

Utilizing React Context and a reducer for state management, my objective is to have the application check if a specific state is empty upon user login. If the state is empty, I want it to be filled with a value, and if it already has a string value, then i ...

Is it feasible to differentiate generic argument as void in Typescript?

One of the functions in my code has a generic type argument. In certain cases, when the context is void, I need to input 0 arguments; otherwise, I need to input 1 argument. If I define the function argument as context: Context | void, I can still add voi ...

Attempting to create a fresh string by substituting certain characters with different ones

Exploring TypeScript, I encountered a puzzle where I needed to substitute specific characters in a string with other characters. Essentially, replacing A with T, T with A, C with G, and G with C. The initial code snippet provided to me looked like this: e ...

Leveraging the power of RXJS and typescript for executing work conditionally between Server and Client Code

I am in need of a function that will assess various conditions to determine if an object is eligible for editing. These conditions may exist on both the server and client sides. Certain conditions should halt further validation and return a value. ...

Encountering an issue while attempting to incorporate an interface within a class in TypeScript

Can someone please help me figure out what I'm doing wrong? I'm attempting to implement an interface inside a class and initialize it, but I keep encountering this error: Uncaught TypeError: Cannot set property 'name' of undefined at n ...

In what situations might a finally block fail to execute?

Are there any circumstances where the code in a finally block may not be reached, aside from the usual suspects like process exit(), termination signal, or hardware failures? In this TypeScript code snippet that usually runs smoothly in node.js, occasiona ...

Unable to execute dockerfile on local machine

I'm currently attempting to run a Dockerfile locally for a Node TypeScript project. Dockerfile FROM node:20-alpine EXPOSE 5000 MAINTAINER Some Dev RUN mkdir /app WORKDIR /app COPY ./backend/* /app RUN npm i CMD ["npm","start"] However, I encoun ...

Error message in Angular 2 RC-4: "Type 'FormGroup' is not compatible with type 'typeof FormGroup'"

I'm currently facing an issue with Angular 2 forms. I have successfully implemented a few forms in my project, but when trying to add this one, I encounter an error from my Angular CLI: Type 'FormGroup' is not assignable to type 'typeo ...

What is the best approach to upgrade this React Native code from version 0.59.10 to version 0.72.5?

I am encountering an issue with the initialRouteName: 'Notifications' property, while working on my React Native code. Despite trying various solutions, I have not been successful in resolving it. As a newcomer to React Native, any assistance wou ...

Utilizing Svelte to Retrieve User Input through Store Functions

Exploring the capabilities of Svelte as a newcomer, I am attempting something that may or may not be achievable, but I remain optimistic! ...

Verify that the password is entered correctly in Angular2

My Angular2 form looks like this: this.registerForm = formBuilder.group({ 'name': ['', Validators.required], 'email': ['', Validators.compose([Validators.pattern("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+&bso ...

Troubleshooting: Issue with Dependency Injection functionality in Angular 2 starter project

I’ve encountered a strange error whenever I attempt to inject any dependency TypeError: Cannot set property 'stack' of undefined at NoProviderError.set [as stack] (errors.js:64) at assignAll (zone.js:704) at NoProviderError.ZoneAwareError (zon ...

Setting up Typescript for a Node.js project configuration

I am facing an issue with my simple class class Blob { } After compiling it with TypeScript, I encountered the following error message: ../../../usr/lib/node_modules/typescript/lib/lib.dom.d.ts:2537:11 2537 interface Blob { ~~~~ ...

`Firebase User Instance and Custom Firestore Document`

Recently, I posted a question regarding Google Firebase Angular Firestore switchMap and encountered some issues. The question can be found here. After exploring AngularFireAuth, I learned that it is used to create a User object with fixed values, requirin ...

When multiple input fields with an iterative design are using the same onChange() function, the specific event.target.values for each input

I'm in the process of developing a dynamic select button that adjusts based on the information entered into the iterative input fields I've set up. These input fields all utilize the same onChange() function. for (let i = 0; i < selectCount; i ...

When utilizing the package, an error occurs stating that Chart__default.default is not a constructor in chart.js

I have been working on a project that involves creating a package with a react chart component using chart.js. Everything runs smoothly when I debug the package in storybook. However, I encountered an error when bundling the package with rollup, referenc ...

Switching out a traditional class component with a functional component within a hook to deduce properties from T

One challenge is to subtract props from T within the withHookFn function using a function instead of a class as successfully done in the withHook function. The code contains comments explaining this issue. Dive into the code for more insights. import Reac ...