是否可以在 Typescript 中使用映射类型来更改类型键名称?

tny*_*nyN 1 typescript

我有大量从 proto 文件生成的打字稿类型。这些类型的属性是驼峰式的,我的 proto 文件(和 api)是蛇形的。

我想避免将我的 api 数据转换为驼峰格式以满足我的类型约束。我试图找出一种使用映射类型将类型键从骆驼更改为蛇案例的方法。

例如:

生成类型

type g = {
    allTheNames: string
}

type SnakePerson = {
    firstName: string
    lastName: string
    name: g
Run Code Online (Sandbox Code Playgroud)

所需类型

{
  first_name: string
  last_name: string
  g: { all_the_names: string }
}
Run Code Online (Sandbox Code Playgroud)

我进行了尝试,但我对打字稿和映射类型相当陌生

type ToSnakeCase<K extends string, T> = {
  [snakeCase([P in K])]: T[P]
}
Run Code Online (Sandbox Code Playgroud)

任何帮助,包括告诉我这是不可能的,都将不胜感激。

jca*_*alz 11

TypeScript 4.1 引入了模板文字类型和映射as子句以及递归条件类型确实允许您实现一个类型函数将驼峰式对象键转换为蛇形键,尽管这种字符串解析代码在不幸的是,编译器遇到了一些相当浅的限制。

首先,我们需要一个CamelToSnake<T>采用驼峰式字符串文字T并生成蛇式字符串的版本。“最简单”的实现看起来像:

type CamelToSnake<T extends string> = string extends T ? string :
    T extends `${infer C0}${infer R}` ? 
    `${C0 extends Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${CamelToSnake<R>}` :
    "";
Run Code Online (Sandbox Code Playgroud)

这里我们T逐个字符地解析。如果字符是大写,我们插入一个下划线。然后我们附加一个小写版本的字符,然后继续。一旦我们有了SnakeToCase我们就可以进行键映射(使用as映射类型中的子句):

type CamelKeysToSnake<T> = {
    [K in keyof T as CamelToSnake<Extract<K, string>>]: T[K]
}
Run Code Online (Sandbox Code Playgroud)

(编辑:如果您需要通过类似 json 的对象递归映射键,您可以改为使用

type RecursiveSnakification<T> = T extends readonly any[] ?
    { [K in keyof T]: RecursiveSnakification<T[K]> } :
    T extends object ? { 
      [K in keyof T as CamelToSnake<Extract<K, string>>]: RecursiveSnakification<T[K]> 
    } : T
Run Code Online (Sandbox Code Playgroud)

但是对于问题中给出的示例类型,非递归映射类型就足够了。)

您可以在示例类型上看到这项工作:

interface SnakePerson {
    firstName: string
    lastName: string
}
   
type CamelPerson = CamelKeysToSnake<SnakePerson>
/* type CamelPerson = {
    first_name: string;
    last_name: string;
} */
Run Code Online (Sandbox Code Playgroud)

不幸的是,如果您的键名超过大约 15 个字符,编译器将无法使用最简单的CamelToSnake实现进行递归:

interface SnakeLengths {
    abcdefghijklmno: boolean;
    abcdefghijklmnop: boolean;
    abcdefghijklmnopq: boolean;
}

type CamelLengths = CamelKeysToSnake<SnakeLengths>
/* type CamelLengths = {
    abcdefghijklmno: boolean;
    abcdefghijklmno_p: boolean;
} */
Run Code Online (Sandbox Code Playgroud)

十六个字符的键被错误地映射,任何更长的东西都完全消失了。为了解决这个问题,你可以开始变得CamelToSnake更复杂;例如,要获取更大的块:

type CamelToSnake<T extends string> = string extends T ? string :
    T extends `${infer C0}${infer C1}${infer R}` ? 
    `${C0 extends Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${C1 extends Uppercase<C1> ? "_" : ""}${Lowercase<C1>}${CamelToSnake<R>}` :
    T extends `${infer C0}${infer R}` ? 
    `${C0 extends Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${CamelToSnake<R>}` :
    "";
Run Code Online (Sandbox Code Playgroud)

这将一个两个而不是一个一个地提取字符,并且只有当您剩下的字符少于两个时才回退到一个一个的版本。这适用于最多约 30 个字符的字符串:

interface SnakeLengths {
    abcdefghijklmno: boolean;
    abcdefghijklmnop: boolean;
    abcdefghijklmnopq: boolean;
    abcdefghijklmnopqrstuvwxyzabcd: boolean;
    abcdefghijklmnopqrstuvwxyzabcde: boolean
    abcdefghijklmnopqrstuvwxyzabcdef: boolean
    abcdefghijklmnopqrstuvwxyzabcdefg: boolean
}

type CamelLengths = CamelKeysToSnake<SnakeLengths>
/*
type CamelLengths = {
    abcdefghijklmno: boolean;
    abcdefghijklmnop: boolean;
    abcdefghijklmnopq: boolean;
    abcdefghijklmnopqrstuvwxyzabcd: boolean; 
    abcdefghijklmnopqrstuvwxyzabcd_e: boolean;
    abcdefghijklmnopqrstuvwxyzabcd_e_f: boolean;
}*/
Run Code Online (Sandbox Code Playgroud)

这对于大多数用途来说可能已经足够了。如果没有,您可以返回并尝试一次提取三个字符而不是一次提取两个字符。或者,您可以尝试避开逐个字符的递归并编写一些在第一个大写字符处破坏字符串的内容,就像在此 GitHub 评论中一样,但这会遇到其他类似的问题。

问题的关键是,TS4.1给你足够的工具,几乎做到这一点,但还不足以做没有一些调整和思想。

Playground 链接到代码