这道题是here题的延伸.

我有一个目标:

type exampleType = {
    propertyOne: string
    propertyTwo: number,
    propertyThree: {
        propertyFour: string,
        propertyFive: Date,
        propertySix: boolean,
    }
}

我正在寻找一种类型,可以将像字符串这样的点符号验证为路径stringDate.在上面的示例中,这意味着该类型将编译为:

propertyOne | propertyThree.propertyFour | propertyThree.PropertyFive

使用前面提出的问题,可以实现以下目标:

type PathsToStringProps<T> = T extends string ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];

type Join<T extends string[], D extends string> =
    T extends [] ? never :
    T extends [infer F] ? F :
    T extends [infer F, ...infer R] ?
    F extends string ? 
    `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;  

type Path = Join<PathsToStringProps<exampleType>, ".">

我正在try 使上面的解决方案成为泛型,这样我就可以为Path提供两个泛型参数:T,在这里代表exampleType,V,在我上面的例子中,它是string|Date.

当我试着让exampleType个通用的时候:

type Path<T> = Join<PathsToStringProps<T>, ".">

我收到了这个错误:Excessive stack depth comparing types 'PathsToStringProps<T>' and 'string[]'.ts(2321)

我可以通过指定T必须表示键值对象来解决这个问题:

type Path<T extends {[key: string]: any}> = Join<PathsToStringProps<T>, ".">

接下来,将值类型限制为路径指向:

type PathsToStringProps<T, V> = T extends (V) ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K], V>]
}[Extract<keyof T, string>];

type Join<T extends string[], D extends string> =
    T extends [] ? never :
    T extends [infer F] ? F :
    T extends [infer F, ...infer R] ?
    F extends string ? 
    `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;  

type Path<T extends {[key: string]: any}, V> = Join<PathsToStringProps<T, V>, ".">

但我得到了一个错误:

error

如果我将泛型参数V从Path中删除,但将其保留在PathsToStringProps中,则它将消失:

type Path<T extends {[key: string]: any}> = Join<PathsToStringProps<T, string|Date>, ".">

Here's a TypeScript Playground of my final attempt at getting this to work.

推荐答案

您的方法是,PathsToProps<T, V>将路径生成为tuples,然后Join<T, D>将元组元素连接在一起形成带点的路径,这对编译器来说是有问题的,因为PathToProps<T, V>Join<T, D>都是recursive conditional types,它们的组合并不总是很好,而且经常会遇到循环保护或性能问题.也许你可以调整一下,让它正常工作,但这不会是我的第一 Select .

相反,因为您看起来根本不关心元组,所以您可以直接在PathsToProps<T, V>内连接字符串.换句话说,不是先构建路径,然后再将它们连接起来,而是在整个过程中进行连接.

它可能如下所示:

type PathsToProps<T, V> = T extends V ? "" : {
    [K in Extract<keyof T, string>]: Dot<K, PathsToProps<T[K], V>>
}[Extract<keyof T, string>];

type Dot<T extends string, U extends string> = 
  "" extends U ? T : `${T}.${U}`

PathsToProps的实现与您的非常相似,不同之处在于,我们显式地使用空字符串""并通过Dot<K, PathsToProps<T[K], V>将它们连接在一起,而不是显式地处理空元组[]并通过[K, ...PathsToProps<T[K], V>>]将其前缀到元组中.

Dot<T, U>型只是连接字符串T和已有虚线的字符串U的速记形式.除非U为空,否则您可以在它们之间放置一个点(如果您有任何以空字符串作为键的对象,这在技术上是错误的.你不知道,是不是?我希望不会).关键是要确保不会出现需要删除的尾随圆点(如果总是用圆点连接,则会得到类似"foo.bar.baz."的路径).

让我们来测试一下:

type ExampleType = {
    propertyOne: string
    propertyTwo: number,
    propertyThree: {
        propertyFour: string,
        propertyFive: Date,
        propertySix: boolean,
    }
}

type StrOrDateEx = PathsToProps<ExampleType, string | Date>
// type StrOrDateEx = "propertyOne" | "propertyThree.propertyFour" | 
//  "propertyThree.propertyFive"

看上go 不错!并且没有递归警告.

Playground link to code

Typescript相关问答推荐

Angular 17 -如何使用新的@if语法在对象中使用Deliverc值

即使子网是公共的,AWS CDK EventBridge ECS任务也无法分配公共IP地址

使Typescribe强制所有对象具有相同的 struct ,从两个选项

如何提取密钥及其对应的属性类型,以供在新类型中使用?

无法从Chrome清除还原的初始状态数据

如何将所有props传递给React中的组件?

使用2个泛型类型参数透明地处理函数中的联合类型

有什么方法可以类型安全地包装import()函数吗?

如果数组使用Reaction-Hook-Form更改,则重新呈现表单

是否将自动导入样式从`~/目录/文件`改为`@/目录/文件`?

对于始终仅在不是对象属性时才抛出的函数,返回Never

Typescript 中相同类型的不同结果

使用或属性编写无限嵌套的接口

递归类型别名的Typescript 使用

为什么看起来相似的代码在打字时会产生不同的错误?

为什么`map()`返回类型不能维护与输入相同数量的值?

TypeScript:方法获胜';t接受较窄类型作为参数

元组解释

如何为字符串模板文字创建类型保护

TypeScript 构造函数参数和接口中的可选属性行为