您可以编写KebabToSnake<T>
实用程序类型,将字符串literal types从kebab case转换为snake case,方法是将破折号替换为下划线:
type KebabToSnake<T extends string, A extends string = ""> =
T extends `${infer L}-${infer R}` ?
KebabToSnake<R, `${A}${L}_`> : `${A}${T}`
这是一个recursive conditional type,它使用template literal types来解析和连接字符串文字.它是一个tail-recursive conditional type,所以它应该表现良好,即使是非常长的弦.
它的工作方式:它接受输入字符串文字类型T
,并构建或accumulates输出字符串文字类型A
.如果T
包含破折号,则它在第一个破折号处将字符串拆分成左侧L
和右侧R
.然后,我们通过使用右侧R
作为要解析的新字符串,并通过将L
和下划线附加到A
作为新的累积输出来递归.如果T
不包含破折号,则只需将T
附加到累计输出并返回即可.
让我们来测试一下:
type Test1 =
KebabToSnake<"a-bc-def-ghij-klm-no-p-q-r-s-t--u---v----x-----y--------z">;
// type Test1 = "a_bc_def_ghij_klm_no_p_q_r_s_t__u___v____x_____y________z";
好的,看起来不错.
然后,我们可以给transform()
一个generic调用签名,该签名接受类型T
的input
对象,并返回mapped type,其中来自T
的每个与string
兼容的属性密钥K
是remapped与KebabToSnake<K>
,并且每个属性值保持不变:
declare function transform<T extends object>(
input: T
): { [K in Extract<keyof T, string> as KebabToSnake<K>]: T[K] }
让我们确保这对呼叫者有效:
const before = {
'first-name': 'Fernando',
'number-of-fingers-and-toes': 20,
'is-a-human-being-or-other-mammal': true
};
const after = transform(before);
/* const after: {
first_name: string;
number_of_fingers_and_toes: number;
is_a_human_being_or_other_mammal: boolean;
} */
看起来不错,输出类型与输入类型相同,只是键已从烤肉串转换为蛇形.
至于实现,这基本上是相同的(除了我在迭代键时使用in
而不是of
;您的是拼写错误吗?)编译器基本上无法验证是否可以将任何特定值赋给复杂的泛型条件类型(如KebabToSnake<T>
)或其重新映射版本.我们不会试图让编译器做一些它不能做的事情,而是通过使用the intentionally unsafe any
type告诉编译器不要担心类型:
function transform<T extends object>(
input: T
): { [K in Extract<keyof T, string> as KebabToSnake<K>]: T[K] } {
const result: any = {};
for (const key in input) {
const parsedKey = parseKey(key);
result[parsedKey] = input[key];
}
return result;
}
编译时没有任何错误.最后,让我们确保它确实像广告中所说的那样有效:
console.log(after.first_name.toUpperCase()); // "FERNANDO"
console.log(after.number_of_fingers_and_toes.toFixed(1)) // "20.0"
看上go 不错.编译器知道after
的类型,并且运行时值与其一致.
Playground link to code个