Typescript - 类型安全深度省略,或 : how to list legal object paths

标签 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>> | [] 解决了这个问题但我不确定这是正确的方法。

总结

那么,有没有更好的方法来强制执行有效路径?是否也可以支持路径联合,以便省略所有路径?现在我得到的结果是不同省略结果的联合。

最佳答案

这对于评论来说太长了,但我不知道它是否算作一个答案。

这是一个从省略并集到多项省略的非常令人讨厌的转换(有许多可能的边缘情况):

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}。很难做到这一点,并且会破坏可选属性,谁知道还有什么。

祝你好运,很抱歉还没有很好的答案。

关于Typescript - 类型安全深度省略,或 : how to list legal object paths,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56118240/

相关文章:

angular - 在不使用 jQuery 将图像上传到服务器之前,如何从输入标签获取 Angular 2(或更高版本)图像的高度和宽度?

python - Python 为对象引用打印 "[...]"是什么意思?

javascript - 如何停止递归和/或使用更新的变量重新启动当前函数

recursion - 使用 XMLUnit 查找缺失元素

python - python函数中的形参和实参

scala - 使用类型别名描述递归语法

java - 数据结构: how do I draw recursion trees?

typescript - 在调用期间解析函数参数类型

javascript - 如何使用 Gulp 将多个 typescript 文件编译成一个 javascript 文件?

javascript - 语法错误: Unexpected token { when running ts-node in Angular Package Format project