是否可以在 Typescript 中使用映射类型来更改类型键名称? [英] Is it possible to use mapped types in Typescript to change a types key names?

查看:22
本文介绍了是否可以在 Typescript 中使用映射类型来更改类型键名称?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

为了满足我的类型限制,我想避免将我的 api 数据转换为驼峰式大小写.我试图找出一种使用映射类型将类型键从骆驼更改为蛇案例的方法.

例如:

生成类型

type g = {allTheNames: 字符串}输入蛇人 = {名字:字符串姓氏:字符串名称:g

所需类型

<代码>{名字:字符串姓氏:字符串g: { all_the_names: 字符串 }}

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

type ToSnakeCase= {[snakeCase([P in K])]:T[P]}

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

解决方案

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

首先,我们需要一个 CamelToSnake,它接受 T 的驼峰式字符串字面量,并生成一个蛇形的版本.最简单"实现看起来像:

type CamelToSnake= 字符串扩展 T ?细绳 :T 扩展 `${infer C0}${infer R}` ?`${C0 extends Uppercase?_":"}${小写}${CamelToSnake}`:";

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

type CamelKeysToSnake= {[K in keyof T as CamelToSnake<Extract<K,string>]:T[K]}

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

type RecursiveSnakification= T 扩展只读 any[] ?{ [K in keyof T]: RecursiveSnakification<T[K]>:T 扩展对象 ?{[K in keyof T as CamelToSnake<Extract<K,string>]:RecursiveSnakification<T[K]>} : T

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

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

interface SnakePerson {名字:字符串姓氏:字符串}type CamelPerson = CamelKeysToSnake/* 输入骆驼人 = {名字:字符串;姓氏:字符串;} */


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

interface SnakeLengths {abcdefghijklmno:布尔值;abcdefghijklmnop:布尔值;abcdefghijklmnopq:布尔值;}type CamelLengths = CamelKeysToSnake/* 输入 CamelLengths = {abcdefghijklmno:布尔值;abcdefghijklmno_p:布尔值;} */

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

type CamelToSnake= 字符串扩展 T ?细绳 :T 扩展 `${infer C0}${infer C1}${infer R}` ?`${C0 extends Uppercase?_":"}${Lowercase}${C1 extends Uppercase?_":"}${小写}${CamelToSnake}`:T 扩展 `${infer C0}${infer R}` ?`${C0 extends Uppercase?_":"}${小写}${CamelToSnake}`:";

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

interface SnakeLengths {abcdefghijklmno:布尔值;abcdefghijklmnop:布尔值;abcdefghijklmnopq:布尔值;abcdefghijklmnopqrstuvwxyzabcd:布尔值;abcdefghijklmnopqrstuvwxyzabcde:布尔值abcdefghijklmnopqrstuvwxyzabcdef:布尔值abcdefghijklmnopqrstuvwxyzabcdefg:布尔值}type CamelLengths = CamelKeysToSnake/*类型骆驼长度 = {abcdefghijklmno:布尔值;abcdefghijklmnop:布尔值;abcdefghijklmnopq:布尔值;abcdefghijklmnopqrstuvwxyzabcd:布尔值;abcdefghijklmnopqrstuvwxyzabcd_e:布尔值;abcdefghijklmnopqrstuvwxyzabcd_e_f:布尔值;}*/

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

关键是,TS4.1 为您提供了足够的工具,几乎可以做到这一点,但如果不进行一些调整和思考,还不足以做到这一点.

游乐场链接到代码

I have a ton of generated typescript types from proto files. These types properties are in camel case and my proto files (and api) are in snake case.

I would like to avoid transforming my api data to camel case in order to satisfy my type constraints. I am trying to figure out a way to use mapped types to change a types keys from camel to snake case.

For example:

Generated Type

type g = {
    allTheNames: string
}

type SnakePerson = {
    firstName: string
    lastName: string
    name: g

Desired Type

{
  first_name: string
  last_name: string
  g: { all_the_names: string }
}

I made an attempt but I am fairly new to typescript and mapped types

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

Any help including telling me this is not possible would be much appreciated.

解决方案

TypeScript 4.1's introduction of template literal types and mapped as clauses and recursive conditional types does allow you to implement a type function to convert camel-cased object keys to snake-cased keys, although this sort of string-parsing code tends to be difficult on the compiler and hits some rather shallow limits, unfortunately.

First we need a CamelToSnake<T> that takes a camel-cased string literal for T and produces a snake-cased version. The "simplest" implementation of that looks something like:

type CamelToSnake<T extends string> = string extends T ? string :
    T extends `${infer C0}${infer R}` ? 
    `${C0 extends Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${CamelToSnake<R>}` :
    "";

Here we are parsing T character-by-character. If the character is uppercase, we insert an underscore. Then we append a lowercase version of the character, and continue. Once we have SnakeToCase we can do the key mapping (using the as clauses in mapped types):

type CamelKeysToSnake<T> = {
    [K in keyof T as CamelToSnake<Extract<K, string>>]: T[K]
}

(Edit: if you need to map the keys recursively down through json-like objects, you can instead use

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

but for the example type given in the question, a non-recursive mapped type will suffice. )

You can see this work on your example types:

interface SnakePerson {
    firstName: string
    lastName: string
}
   
type CamelPerson = CamelKeysToSnake<SnakePerson>
/* type CamelPerson = {
    first_name: string;
    last_name: string;
} */


Unfortunately, if your key names are longer than about fifteen characters, the compiler loses its ability to recurse with the simplest CamelToSnake implementation:

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

type CamelLengths = CamelKeysToSnake<SnakeLengths>
/* type CamelLengths = {
    abcdefghijklmno: boolean;
    abcdefghijklmno_p: boolean;
} */

The sixteen-character key gets mapped incorrectly, and anything longer disappears entirely. To address this you can start making CamelToSnake more complicated; for example, to grab bigger chunks:

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>}` :
    "";

This pulls off characters two-by-two instead of one-by-one, and only falls back to the one-by-one version if you have fewer than two characters left. This works for strings up to about 30 characters:

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;
}*/

That's probably enough for most uses. If not, you could go back and try pulling off characters three at a time instead of two at a time. Or you could try to sidestep the character-by-character recursion and write something that breaks a string at the first uppercase character, like in this GitHub comment, but that runs into other similar issues.

The point is, TS4.1 gives you enough tools to pretty much do this, but not enough to do it without some tweaking and thought.

Playground link to code

这篇关于是否可以在 Typescript 中使用映射类型来更改类型键名称?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆