Utilizing keyof in Typescript with additional type conditions

I am trying to pass two generic conditions for an array type field name, but the second condition is not being accepted.

This is my method declaration and there doesn't seem to be a problem with it.

firstOrDefault<K extends keyof T>(predicate?: (item: T) => boolean, recursiveItem?: K): T;

The above method declaration is functioning properly. However, I only want to pass an Array type in the recursiveItem field.

I attempted the following method declaration, but it did not work as expected.

firstOrDefault<K extends keyof T & T[]>(predicate?: (item: T) => boolean, recursiveItem?: K): T

Could someone provide guidance on resolving this issue?

Sample Code

let departments : IDepartment[] = [
    {
        name: 'manager',
        subDepartments: [
            {
                name: 'accountant'
            }
        ]
    }
]

// The code snippet above worked, but ideally I want to only pass fields of type T[] such as subDepartments in the recursiveItem parameter.
let department = departments.firstOrDefault(d => d.name == 'accountant', 'subDepartments')
console.log(department)

interface Array<T> {
    firstOrDefault<K extends keyof T>(predicate?: (item: T) => boolean, recursiveItem?: K): T;
}

Array.prototype.firstOrDefault = function(this, predicate, recursiveItem) {
    if (!predicate)
        return this.length ? this[0] : null;
    for (var i = 0; i < this.length; i++) {
        let item = this[i]
        if (predicate(item))
            return item
        if (recursiveItem) {
            let subItems = item[recursiveItem]
            if (Array.isArray(subItems)) {
                var res = subItems.firstOrDefault(predicate, recursiveItem)
                if (res)
                    return res
            }
        }
    }
    return null;
}

interface IDepartment {
    name?: string,
    subDepartments?: IDepartment[]
}

Answer №1

Check out this type definition recommendation

type ArrayProperties<T, I> = { [K in keyof T]: T[K] extends Array<I> ? K : never }[keyof T]

class X {
    numbers: number[];
    letters: string[];
    str: string;
    num: number;
    action() {}
}

let allArrays: ArrayProperties<X, any>; // "numbers" | "letters"
let numberArray: ArrayProperties<X, number>; // "numbers"

For better clarity, here is the revised firstOrDefault function with a restriction for recursiveItem:

function firstOrDefault<T>(predicate?: (item: T) => boolean, recursiveItem?: ArrayProperties<T, T>): T { }

Applied to your example:

interface IDepartment {
    name: string;
    subDepartments?: IDepartment[];
}

let departments : IDepartment[] = [
    {
        name: 'manager',
        subDepartments: [
            {
                name: 'accounting'
            }
       ]
    }
]

let resultA = firstOrDefault(((d: IDepartment) => d.name === 'accounting'), 'subDepartments'); // Good
let resultB = firstOrDefault(((d: IDepartment) => d.name === 'accounting'), 'subDepartment'); // issue: [ts] Argument of type '"subDepartment"' is not suitable for parameter of type '"subDepartments"'.

Answer №2

Finding a suitable solution may be challenging in this case. You are essentially searching for a type function that can identify the properties of a specific type T whose values belong to the type Array<T> (or possibly Array<T> | undefined given the optional nature of some properties). This could ideally be achieved using mapped conditional types, which unfortunately are not yet integrated into TypeScript. A potential approach could look something like this:

type ValueOf<T> = T[keyof T];
type RestrictedKeys<T> = ValueOf<{
  [K in keyof T]: If<Matches<T[K], Array<T>|undefined>, K, never>
}>

You could then specify the recursiveItem parameter as having the type RestrictedKeys<T>, but regrettably, this method is currently not feasible.


An alternate approach I have found effective involves refraining from extending the Array prototype. It is generally considered poor practice anyway. Instead, consider implementing a standalone function with an Array<T> as its first parameter, like so:

function firstOrDefault<K extends string, T extends Partial<Record<K, T[]>>(arr: Array<T>, pred?: (item: T) => boolean, rec?: K): T | null {
    if (!pred)
        return this.length ? this[0] : null;
    for (var i = 0; i < this.length; i++) {
        let item = this[i]
        if (pred(item))
            return item
        if (rec) {
            let subItems = item[rec]
            if (Array.isArray(subItems)) {
                var res = firstOrDefault(subItems, pred, rec)
                if (res)
                    return res
            }
        }
    }
    return null;
}

In this instance, you can constrain the type T to be a

Partial<Record<K, T[]>>
, indicating that T[K] is an optional property consisting of type Array<T>. By imposing this restriction on T, the type checker behaves according to expectations.

firstOrDefault(departments, (d: IDepartment) => d.name == 'accountant', 'subDepartments') // valid
firstOrDefault(departments, (d: IDepartment) => d.name == 'accountant', 'name') // error
firstOrDefault(departments, (d: IDepartment) => d.name == 'accountant', 'random') // error

It must be noted that adapting the aforementioned solution to extend the Array<T> interface presents significant challenges since it relies on restricting the type T. While theoretically possible to define K in terms of T, such as

keyof (T & Partial<Record<K, T[]>>
, TypeScript's limited ability to eliminate impossible types renders this approach inadequate.

In conclusion, considering the previous solution while also reevaluating your requirements—such as altering the recursiveItem parameter—not as a key name, might be beneficial. Opting for the standalone function solution ensures adherence to your initial intentions and avoids contaminating the Array prototype. The choice ultimately rests with you. Best of luck!


UPDATE: It appears you have resolved the issue by adjusting a different aspect of the requirement: omitting the need for the recursiveItem parameter to represent a key name. Nonetheless, I recommend contemplating the standalone function approach, which aligns with your original intention and circumvents modifications to the Array prototype. The decision is yours to make. Good luck once again!

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

When using the subscribe method in Angular4, I am encountering a null value for the data

After receiving data from my subscription and displaying it, I encounter an issue when calling my function as it returns a "null" value. I apologize for any language errors in my message. Thank you. this.service.prepareNewVersion().subscribe(data2 => ...

Could we bypass the parent component somehow?

Just starting out with angular and decided to work on a small project. As I was setting up my routing, I encountered a problem. Here is the routing setup I have: const routes: Routes = [ {path: '', redirectTo: '/home', pathMatch: &a ...

Error in Angular 2: Component unable to locate imported module

I'm facing an issue where a module I want to use in my application cannot be found. The error message I receive is: GET http://product-admin.dev/node_modules/angular2-toaster/ 404 (Not Found) The module was installed via NPM and its Github reposito ...

Is the regex returning the correct result?

I need to discuss the date field with a format of YYYYMMDD, as shown below: zod.string().max(8).regex(new RegExp('^(19[0-9][0-9]|20[0-9][0-9]|[0-1][0-9]{3})(1[0-2]|0[1-9])(3[01]|[0-2][1-9]|[12]0)$')); The value provided is 20001915. The definit ...

Having trouble entering text into a React input field

Encountering a puzzling issue with a simple form featuring an input field that inexplicably won't respond to keyboard typing. Initially, suspicions pointed towards potential conflicts with the onChange or value props causing the input to be read-only. ...

Maximizing the efficiency of enums in a React TypeScript application

In my React application, I have a boolean called 'isValid' set like this: const isValid = response.headers.get('Content-Type')?.includes('application/json'); To enhance it, I would like to introduce some enums: export enum Re ...

Having trouble with the lodash find function in my Angular application

npm install lodash npm install @types/lodash ng serve import { find, upperCase } from 'lodash'; console.log(upperCase('test')); // 'TEST' console.log(find(items, ['id', id])) // TypeError: "Object(...)(...) is un ...

a guide to caching a TypeScript computed property

I have implemented a TypeScript getter memoization approach using a decorator and the memoizee package from npm. Here's how it looks: import { memoize } from '@app/decorators/memoize' export class MyComponent { @memoize() private stat ...

Error: Unable to iterate over data.data due to its type

I am attempting to fetch images from the woocommerce API and here is the code I am using: this.config.getWithUrl(this.config.url + '/api/appsettings/get_all_banners/?insecure=cool') .then((data: any) => { this.banners = data.data; consol ...

Utilizing variables to set the templateUrl in Angular2

Trying to assign a variable to the templateUrl in my component, but it's not functioning as expected. @Component({ selector: 'article', templateUrl: '{{article.html}}', styleUrls: ['styles/stylesheets/article.comp ...

Tips for properly sending a JWT token

When working on my angular application, I encountered an issue while trying to send a jwt token as a header for request authorization. The error 500 was triggered due to the jwt token being sent in an incorrect format. Here is how I am currently sending it ...

Create an array using modern ES6 imports syntax

I am currently in the process of transitioning Node javascript code to typescript, necessitating a shift from using require() to import. Below is the initial javascript: const stuff = [ require("./elsewhere/part1"), require("./elsew ...

What is the method for implementing type notation with `React.useState`?

Currently working with React 16.8.3 and hooks, I am trying to implement React.useState type Mode = 'confirm' | 'deny' type Option = Number | null const [mode, setMode] = React.useState('confirm') const [option, setOption] ...

Unpacking objects in Typescript

I am facing an issue with the following code. I'm not sure what is causing the error or how to fix it. The specific error message is: Type 'CookieSessionObject | null | undefined' is not assignable to type '{ token: string; refreshToken ...

Guide to resolving the issue of error Type 'void[] | undefined' cannot be assigned to type 'ReactNode'

I am attempting to map the data Array but I am encountering an error: Type 'void[] | undefined' is not assignable to type 'ReactNode'. Can someone please assist me in identifying what I am doing wrong here? Below is the code snippet: i ...

The callback function inside the .then block of a Promise.all never gets

I'm currently attempting to utilize Promise.all and map in place of the forEach loop to make the task asynchronous. All promises within the Promise.all array are executed and resolved. Here is the code snippet: loadDistances() { //return new Prom ...

What is the process for developing an interface adapter using TypeScript?

I need to update the client JSON with my own JSON data Client JSON: interface Cols { displayName: string; } { cols:[ { displayName: 'abc'; } ] } My JSON: interface Cols { label: string; } { cols:[ { label:&a ...

What could be the reason for the component not receiving data from the service?

After attempting to send data from one component to another using a service, I followed the guidance provided in this answer. Unfortunately, the data is not being received by the receiver component. I also explored the solution suggested in this question. ...

Conceal a designated column within a material angular data table based on the condition of a variable

In the morning, I have a question about working with data tables and API consumption. I need to hide a specific column in the table based on a variable value obtained during authentication. Can you suggest a method to achieve this? Here is a snippet of my ...

Can you tell me the appropriate type for handling file input events?

Using Vue, I have a simple file input with a change listener that triggers the function below <script setup lang="ts"> function handleSelectedFiles(event: Event) { const fileInputElement = event.target as HTMLInputElement; if (!fileInp ...