打字键多余类型条件

时间:2017-09-10 10:31:54

标签: typescript typescript2.4

我希望传递两个通用条件用于传递数组类型字段名但不接受第二个条件。

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

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

2 个答案:

答案 0 :(得分:2)

我不认为这是一个很好的答案。您正在寻找的是一个类型函数,它标识类型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;
}

在上文中,您可以将类型T限制为Partial<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起作用。从理论上讲,你可以用K来表达T,就像keyof (T & Partial<Record<K,T[]>>一样,但是TypeScript没有积极地评估交叉点以消除不可能的类型,所以这仍然接受name,甚至虽然name属性的推断类型类似于string & IDepartment[],但不应该存在。

无论如何,希望以上解决方案能为您服务。祝你好运!

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

答案 1 :(得分:2)

尝试此类型定义

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