根据第一个(包含对象点表示法)推断第二个元组元素类型



上下文

我正在尝试创建一个类型安全的路径段数组,以深入对象。对于这个特定的接口,只有两个层次的深度是我试图构建类型所依据的。我最终会使用这些段使用点表示法对对象进行索引,但目前,我只是想确保类型受到足够的约束,这样就不会添加不正确的路径。

示例

interface Days {
monday: string;
tueday: string;
wednesday: string;
thursday: string;
friday: string;
saturday: string;
sunday: string;
}
interface Weekend {
saturday: string;
sunday: string;
}
interface Example {
days: Days;
weekend: Weekend;
year: string;
}
type KeysOfUnions<T> = T extends T ? keyof T : never;
type ExamplePath<T extends keyof Example = keyof Example> = [T, KeysOfUnions<Example[T]>?];
const correctlyErrors: ExamplePath = ["days", "test"]; // good - this errors so we're catching bad paths
const allowsCorrectPath: ExamplePath = ["days", "monday"]; // good - valid paths are accepted
const allowsIncorrectPaths: ExamplePath = ["weekend", "monday"]; // bad! - invalid combinations of paths are allowed

到目前为止,我提出的类型过于松散,允许对路径段进行任何排列,即使这些排列是不可能的(即["weekend", "monday"](。我尝试使用一个具有元组类型的泛型类型变量,在获取其键之前,使用第一个路径段作为类型T来索引到Example类型。

这种索引方法的结果类型是以下各项的结合:

(Days | Weekend | string)

在此并集类型上使用keyof,导致错误

Type 'string' is not assignable to type 'never'.ts(2322)

因此,使用条件类型KeysOfUnions来获取每个联合成员的键,这导致了您可以想象的过于松散的类型。

问题

如何使用第一个元素推断元组的第二个元素(路径段(,确保类型系统强制只能添加路径段的有效组合?

编辑1:我还在寻找一种解决方案,如果没有更多的属性可供挖掘,则可以使用单个分段。即["year"],理想情况下向阵列添加任何更多元素将破坏类型。

编辑2:一个可能不那么小的附录😅给出的例子是一个有2个嵌套级别的虚构接口,然而,我在问题中过于简化了它的结构,实际接口大约有5个嵌套级别。例如,假设那些DaysWeekend示例接口要深入得多,每天都包含子对象等等。实际上,我的解决方案是键入一个元组/数组,只深入到两个级别的属性,忽略更深的路径段。因此,对于这种约束,递归方法可能是不可能的。

当我们对这个问题进行分解时,它变得非常简单,而您实际上非常接近;您只需要分别获取每个路径,而不是将它们全部放入一个元组中。在这里,我选择使用映射类型(但也可以使用分布式条件类型(:

type KeyPaths<T> = {
[K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K]>] : [K];
}[keyof T];
type ExamplePath = KeyPaths<Example>;

本质上,对于T的每个键,我们都在检查T[K]是否是对象,如果是,我们将进一步深入对象。否则,我们只给[K]

它适用于给定的例子,也给出了有用的错误:

键入";测试"不可分配给类型";星期一"tueday"|"星期三"星期四"星期五"星期六"星期日未定义"。(2322(

和第二个:

键入";"周末"不可分配给类型";天"。(2322(

游乐场


也可以为类型添加递归限制:

type KeyPaths<T, Depth extends unknown[]> = Depth extends [] ? [] : {
[K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K], Depth extends [...infer D, any] ? D : never>] : [K];
}[keyof T];
type ExamplePath = KeyPaths<Example, [0, 0]>;

在这里,我们只是使用元组的长度来跟踪剩余的递归,直到我们完成为止。

如果这对你没有吸引力,因为它有点脏,你也可以使用数字,并索引到一个元组中;递减";它们:

type Decrement<X extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9][X];
type KeyPaths<T, Depth extends number> = Decrement<Depth> extends -1 ? [] : {
[K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K], Decrement<Depth>>] : [K];
}[keyof T];
type ExamplePath = KeyPaths<Example, 2>;

但是,这受到Decrement类型中包含的元素数量的限制。可以通过更多的类型操作来消除这一限制,但这超出了这个问题的范围,也没有必要。

您希望ExamplePath分布式对象类型(如ms/TS#47109中所创造的(,其中您将类型[K, keyof Example[K]]分布在联合keyof Example中的每个K上。它看起来像这样:

type ExamplePath = { [K in keyof Example]: [K, keyof Example[K]] }[keyof Example]
/* type ExamplePath = ["days", keyof Days] | ["weekend", keyof Weekend] | 
["year", number | typeof Symbol.iterator | "toString" | "charAt" | 
"charCodeAt" | "concat" | ... 37 more ... | "padEnd"] */

这为您提供了想要的"days""weekend"行为(不确定"year",因为第二个元素是keyof string,这是所有string方法和明显属性的噩梦,但这显然是您想要的,所以👍😬)

无论如何,让我们测试一下:

const correctlyErrors: ExamplePath = ["days", "test"]; // error
const allowsCorrectPath: ExamplePath = ["days", "monday"]; // no error
const alsoErrors: ExamplePath = ["weekend", "monday"]; // error

看起来不错。

游乐场链接到代码

相关内容

最新更新