上下文
我正在尝试创建一个类型安全的路径段数组,以深入对象。对于这个特定的接口,只有两个层次的深度是我试图构建类型所依据的。我最终会使用这些段使用点表示法对对象进行索引,但目前,我只是想确保类型受到足够的约束,这样就不会添加不正确的路径。
示例
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个嵌套级别。例如,假设那些Days
和Weekend
示例接口要深入得多,每天都包含子对象等等。实际上,我的解决方案是键入一个元组/数组,只深入到两个级别的属性,忽略更深的路径段。因此,对于这种约束,递归方法可能是不可能的。
当我们对这个问题进行分解时,它变得非常简单,而您实际上非常接近;您只需要分别获取每个路径,而不是将它们全部放入一个元组中。在这里,我选择使用映射类型(但也可以使用分布式条件类型(:
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
看起来不错。
游乐场链接到代码