如何在Typescript中使用类型推断转换对象字段名称?

How to transform object field name using type infer in Typescript?

提问人:Beeno Tung 提问时间:10/29/2023 更新时间:10/30/2023 访问量:92

问:

我已经实现了一个具有转换对象键的解析器构建器(它在 js 中工作),但难以转换推断类型的对象键。

示例代码:

let sampleData = {
  id: 1,
  delete_time$optional: new Date()
}
let parser = inferFromSampleValue(sampleData)

构造的解析器在输入对象中需要一个数字和一个可选的 Date。iddelete_time

在我的初始实现中:

type Parser<T> = {
  // throw runtime error if type not matched
  parse(input: unknown): T
  // typescript type of expected data in string
  type: string
}
function inferFromSampleValue<T>(value: T): Parser<T> {
  // implementation omitted
}

解析器结果的类型签名为:

{
  id: number
  delete_time$optional: Date
}

但所需的类型签名应该是:

{
  id: number
  delete_time?: Date
}

然后我尝试编写一些泛型类型:


type InferFieldName<S> = S extends `${infer N}$optional` ? N : S

type ExcludeOptionalFieldName<S> = S extends `${infer N}$optional` ? never : S

type InferType<T> = T extends Record<infer K extends keyof T, infer V>
  ? Partial<Record<InferFieldName<K>, V>> &
      Pick<T, ExcludeOptionalFieldName<keyof T>>
  : T

function inferFromSampleValue<T>(value: T): Parser<InferType<T>> {
  // implementation omitted
}

然后,解析器结果的类型签名变为:

{
  id: number
  delete_time?: number | Date
}

这仍然不是预期的结果,因为它混合了可选字段的所有字段的类型(它推断为可选而不是可选)。number | DateDate

我尝试做如下操作,但 Typescript 中的语法似乎无效:

type InferType<T> = T extends {}
  ? {
      [InferFieldName<P> where P in keyof T]: T[P]
    }
  : T

上面发布的示例代码是简化版本,实际代码涉及更多类型的转换(对于可为 null 的字段和枚举)并以递归方式应用。

这是针对在 github 和 npm 上发布的开源项目“cast.ts”。链接到相关行: https://github.com/beenotung/cast.ts/blob/644e5bf/src/core.ts#L1067-L1154

谢谢你的帮助。

TypeScript 对象 元编程 TypeScript-generics 类型推理

评论

1赞 jcalz 10/29/2023
这种方法是否满足您的需求?如果是这样,我会写一个答案来解释;如果没有,我错过了什么?
0赞 Beeno Tung 10/30/2023
谢谢,您的方法与我正在寻找的方式完全一样。不过,我们似乎不需要转义 $ 字符。
0赞 Beeno Tung 10/30/2023
我更喜欢在 inferFromSampleValue() 的返回值中使用 Convert<T> 而不是 Parser.parse(),但这是一个小偏好,你解决了我的难题:)
0赞 jcalz 10/30/2023
是的,你不需要逃避;不过,我的IDE变成了无法逃脱的红色,大概是为了鼓励逃跑。$$

答:

0赞 jcalz 10/30/2023 #1

一种方法是给出以下调用签名:inferFromSampleValue()

declare function inferFromSampleValue<T>(value: T): Parser<
    { [K in keyof T as K extends `${string}\$optional` ? never : K]: T[K] } &
    { [K in keyof T as K extends `${infer P}\$optional` ? P : never]?: T[K] }
>;

这将使用映射类型中的键重新映射来筛选以 . 结尾或不以 . 结尾的键在第一种情况下,它会过滤这些键,因此您最终只能获得非可选属性。在第二种情况下,它会过滤这些键,但会删除后缀。此外,第二种情况使用映射修饰符,以便生成的类型具有所有可选属性。这两种情况相在一起,以创建具有非可选属性和可选属性的单个对象类型的等效项,如预期所示:"$optional""$optional"?

let sampleData = {
    id: 1,
    delete_time$optional: new Date()
}
let parser = inferFromSampleValue(sampleData);
declare let input: unknown
const v = parser.parse(input);
//    ^? const v: { id: number; } & { delete_time?: Date | undefined; }
v.id.toFixed(1); // okay
v.delete_time?.getFullYear(); // okay

看起来不错!

Playground 代码链接