4

I want pass two generic condition for pass array type field name but not accepted second condition.

This my method declaration and this no problem.

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

Above method declaration is working but I want pass only Array type in recurviseItem field.

I'm trying this method declaration but doesn't work.

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

How can solve this problem?

Sample Code

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

// This my method declaration and this code worked but you can pass name and subDepartments field pass for recursiveItem parameter but i want only T[] type field pass so only subDepartments.
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[]
}
9
  • Shouldn't that be recursiveItem?: K[], then? That would be an array of things that are keys of T. Commented Sep 10, 2017 at 10:33
  • What is the specific problem you are having, Tried to compile your code in ts 2.5 and both versions work (I assumed they were declared in a generic type with a T parameter, Commented Sep 10, 2017 at 10:36
  • do you want to say that recursiveItem can be either an array of T or a key of T ? Commented Sep 10, 2017 at 10:38
  • firstOrDefault is an array prototype method and i want only array type field for recursiveItem parameter. Commented Sep 10, 2017 at 10:41
  • @jonrsharpe thanks for your comment, your suggestion unfortunately incorrent because K[] declaration only was such as fieldName[] for example i'm passed firstName for recursiveItem parameter and result that => 'firstName'[] unfortunately incorrect. I want this result => T['fieldName']:T[] Commented Sep 10, 2017 at 10:56

2 Answers 2

8

Try this type definition

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

class A {
    arr1: number[];
    arr2: string[];
    str: string;
    num: number;
    func() {}
}

let allArr: ArrayProperties<A, any>; // "arr1" | "arr2"
let numArr: ArrayProperties<A, number>; // "arr1"

so firstOrDefault would look like that (I put ArrayProperties<T, T> to restrict recursiveItem only for recursive type i.e. only properties of type IDepartment[] could be used, however you can put ArrayProperties<T, any> if you want to accept any array)

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

With your example

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

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

let a = firstOrDefault(((d: IDepartment) => d.name === 'accountant'), 'subDepartments'); // OK
let b = firstOrDefault(((d: IDepartment) => d.name === 'accountant'), 'subDepartment'); // error: [ts] Argument of type '"subDepartment"' is not assignable to parameter of type '"subDepartments"'.
Sign up to request clarification or add additional context in comments.

Comments

2

I don't think there's a great answer. What you are looking for is a type function that identifies the properties of a type T whose values are of type Array<T> (or Array<T> | undefined since optional properties are like that). This would be most naturally expressed by mapped conditional types, which are not yet part of TypeScript. In that case, you could make something like

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

and annotate the recursiveItem parameter as type RestrictedKeys<T> and be done. But you can't do that.


The only solution I've that actually works is to give up on extending the Array prototype. (That's bad practice anyway, isn't it?) If you are okay with a standalone function whose first parameter is an Array<T>, then you can do this:

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 the above, you can restrict the type T to be a Partial<Record<K,T[]>>, meaning that T[K] is an optional property of type Array<T>. By expressing this as a restriction on T, the type checker behaves as you'd like:

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

As I said, there's no great way to take the above solution and make it work for extending the Array<T> interface, since it works by restricting T. In theory, you could express K in terms of T, like keyof (T & Partial<Record<K,T[]>>, but TypeScript does not aggressively evaluate intersections to eliminate impossible types, so this still accepts name, even though the inferred type of the name property would be something like string & IDepartment[] which shouldn't exist.

Anyway, hope the above solution can work for you. Good luck!


EDIT: I see you've solved your own problem by relaxing a different requirement: the recursiveItem parameter is no longer a key name. I still think you should consider the standalone function solution, since it works as you originally intended and doesn't pollute the prototype of Array. It's your choice, of course. Good luck again!

2 Comments

Thanks for answer but i don't see your partial and record object. I want try your suggestion.
I'm sorry, i'm finded partial and record object from this url typescriptlang.org/docs/handbook/release-notes/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.