额外类型条件的 Typescript key

Typescript keyof extra type condition

我想传递数组类型字段名称的两个通用条件,但不接受第二个条件。

这是我的方法声明,没问题。

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

上面的方法声明有效,但我只想在 recurviseItem 字段中传递 Array 类型。

我正在尝试此方法声明但不起作用。

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

如何解决这个问题?

示例代码

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[]
}

我认为没有很好的答案。您正在寻找的是一个类型函数,它标识类型 T 的属性,其值是 Array<T> 类型(或 Array<T> | undefined 因为可选属性就是这样)。这最自然地用 mapped conditional types 表示,它还不是 TypeScript 的一部分。在那种情况下,你可以做类似

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

并将 recursiveItem 参数注释为类型 RestrictedKeys<T> 并完成。但你不能那样做。


我唯一实际可行的解决方案是放弃扩展 Array 原型。 (不管怎么说,这都是不好的做法,不是吗?)如果您可以使用第一个参数是 Array<T> 的独立函数,那么您可以这样做:

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;
}

在上面,你可以限制类型 TPartial<Record<K,T[]>>,这意味着 T[K] 是类型 Array<T> 的可选 属性 .通过将此表示为对 T 的限制,类型检查器将按照您的意愿运行:

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

正如我所说,没有很好的方法来采用上述解决方案并使其适用于扩展 Array<T> 接口,因为它通过限制 T 来工作。理论上,您可以用 T 来表达 K,例如 keyof (T & Partial<Record<K,T[]>>,但 TypeScript 不会积极评估交集以消除不可能的类型,因此它仍然接受 name,即使尽管 name 属性 的推断类型类似于 string & IDepartment[] 不应该存在。

无论如何,希望以上解决方案对您有用。祝你好运!


编辑:我看到您通过放宽不同的要求解决了您自己的问题:recursiveItem 参数不再是键名。我仍然认为您应该考虑独立函数解决方案,因为它按您最初的预期工作并且不会污染 Array 的原型。当然,这是你的选择。再次祝你好运!

试试这个类型定义

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"

所以 firstOrDefault 看起来像那样(我把 ArrayProperties<T, T> 限制为递归类型的 recursiveItem,即只能使用 IDepartment[] 类型的属性,但是你可以把 ArrayProperties<T, any> 如果你想接受任何数组)

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

以你为例

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"'.