打字稿-键入安全的深省略,或:如何列出合法对象路径

时间:2019-05-13 18:47:22

标签: typescript recursion recursive-datastructures typescript-generics

好的,这是一个很长很具体的内容。非常感谢您的投入。

我写了一个递归Omit类型,它使用一个T类型和一个字符串元组(“路径”),并索引到T中,删除了最后一个项目在路径上并返回该类型。

我已经看过Deep Omit With Typescript了-但这涉及递归地省略相同的键,而不是沿路径遍历。

我也看过Type Safe Omit Function,但这涉及到运行时,大约是一个一级的省略。

我的实现:

// for going along the tuple that is the path
type Tail<T extends any[]> = ((...args: T) => void) extends ((head: unknown, ...rest: infer U) => void) ? U : never;

type DeepOmit<T, Path extends string[]> = T extends object ? {
    0: Omit<T, Path[0]>;
    1: { [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K] }
}[Path['length'] extends 1 ? 0 : 1] : T;

我想这样做,因此Path被约束为对T的有效遍历,例如:

Path = [K1, K2, K3, ..., K_n] such that:
K1 extends keyof T, K2 extends keyof T[K1], ... Kn extends keyof[T][K1]...[K_(n-1)]

首次尝试

我编写了一个类型,该类型采用类型T和路径P,如果有效,则返回P,否则返回never

type ExistingPathInObject<T, P extends any[]> = P extends [] ? P : {
    0: ExistingPathInObject<T[P[0]], Tail<P>> extends never ? never : P;
    1: never;
}[P[0] extends keyof T ? 0 : 1]

但是,在DeepOmit的签名中,我无法强制执行Path extends ExistingPathInObject<T,Path>,因为这是循环依赖项。我之所以提及这种尝试,是因为可能有一种方法可以规避圆度并使用此类型来验证Path是对T的有效遍历。

第二次尝试

因为我似乎无法使用Path来约束自己,所以我尝试生成类型中所有现有路径的并集,然后要求Path进行扩展。我能想到的最好的方法是:

type Paths<T> = {
    0: { [K in keyof T]: T[K] extends object ? [K, Paths<T[K]>[keyof Paths<T[K]>]] : [K] }
    1: []
}[T extends object ? 0 : 1];

// an example type to test on
type HasNested = { a: string; b: { c: number; d: string } };

type pathTest = Paths<HasNested>;
//{
//  a: ["a"];
//  b: ["b", ["d"] | ["c"]];
//}

type pathTestUnion = Paths<HasNested>[keyof Paths<HasNested>]
// ["a"] | ["b", ["d"] | ["c"]]

这使我可以匹配写为树的路径:['a'] extends pathTestUnion['b', ['d']] extends pathTestUnion都是正确的。我必须添加一个[keyof T]才能获得并集,但我不能将其放在类型Paths中,因为它未被识别为有效。

完成所有这些操作后,我现在很难重写DeepOmit以使用此约束。这是我尝试过的:

type Types<T> = T[keyof T];

type TypeSafeDeepOmit<T, Path extends Types<Paths<T>>, K extends keyof T> =
    Path extends any[] ?
    T extends object ?
    { // T is object and Path is any[]
        0: Omit<T, K>;
        1: { [P in keyof T]: P extends Path[0] ? TypeSafeDeepOmit<T[P], Path[1], Path[1][0]> : T[P] }
    }[Path['length'] extends 1 ? 0 : 1] :
    T : // if T is not object
    never; // if Path is not any[]

type TSDO_Helper<T, P extends Types<Paths<T>>> = P extends any[] ? P[0] extends keyof T ? TypeSafeDeepOmit<T, P, P[0]> : never : never;

这很丑陋,并且使用助手类型来实际工作。我还必须告诉编译器P extends any[]P[0] extends keyof T,尽管Paths正是要确保这一点。使用TypeSafeDeepOmit-

进行的Path[1]递归调用中也出现了错误
Type 'any[] & Path' is not assignable to type '[]'.
        Types of property 'length' are incompatible.
          Type 'number' is not assignable to type '0'

我已通过设置Path extends Types<Paths<T>> | []来解决此问题,但是我不确定这是正确的方法。

总结

那么,有没有更好的方法来强制执行有效路径?是否还可以支持路径的合并,从而将所有路径都忽略掉?现在,我得到的结果是各种省略结果的结合。

1 个答案:

答案 0 :(得分:2)

这个评论太长了,但是我不知道它是否可以作为答案。

这是一个真正的令人讨厌的转换(有许多可能的极端情况),从省略组合到多省略:

type NestedId<T, Z extends keyof any = keyof T> = (
  [T] extends [object] ? { [K in Z]: K extends keyof T ? NestedId<T[K]> : never } : T
) extends infer P ? { [K in keyof P]: P[K] } : never;

type MultiDeepOmit<T, P extends string[]> = NestedId<P extends any ? DeepOmit<T, P> : never>

它的工作方式(如果可以工作)是采用{a: string, b: number} | {b: number, c: boolean}之类的联合,并且仅使用存在于联合的所有组成部分中的键:{b: number}。很难做到这一点,并且破坏了可选属性以及谁知道其他什么。

祝你好运,对不起,没有好的答复。