本文将会手把手带你解锁一道 TypeScript 类型挑战题 ——《实现 Camelize 函数》。
虽然难度为【困难】,但期间会对用到的 ts 知识(类型体操基础动作)做学习和讲解,理解起来依然轻松,让你从此不再害怕类型体操。
type challenge[1] 是一个 TypeScript 类型体操姿势合集。里面有「简单」「中等」「困难」「地狱」四个等级的题目。
今天我们来完成其中的一道困难级别题目:实现 Camelize 函数。
原题地址:https://github.com/type-challenges/type-challenges/blob/main/questions/01383-hard-camelize/README.md
题目如下:
实现 Camelize 类型: 将对象属性名从 蛇形命名(下划线命名) 转换为 小驼峰命名
Camelize<{
some_prop: string,
prop: { another_prop: string },
array: [{ snake_case: string }]
}>
// expected to be
// {
// someProp: string,
// prop: { anotherProp: string },
// array: [{ snakeCase: string }]
// }
为了了解 camelize 的实现原理,我们先用 js 自己实现一下。
import { camelCase } from 'lodash';
export const isPlainObjectX = (obj) => Object.prototype.toString.call(obj) === '[object Object]';
function camelize(obj) {
// 如果是数组,遍历执行 camelize
if (Array.isArray(obj)) {
return obj.map(item => camelize(item));
// 如果是对象
} else if (isPlainObjectX(obj)) {
const newObj = Object.create(null);
Object.keys(obj).forEach(key => {
// 将 key 改为驼峰,对 value 递归 camelize
newObj[camelCase(key)] = camelize(obj[key]);
});
return newObj;
}
// 其余情况,不处理
return obj;
}
理一下逻辑:
可以看到 camelize 的实现依赖 camelCase,camelCase 来自于 lodash。
但 ts 类型里没有 lodash,因此我们也首先用 ts 类型来实现 CamelCase。
该题也是 ts 类型挑战中难度为 Hard 类型的题目。
原题地址:https://github.com/type-challenges/type-challenges/blob/main/questions/00114-hard-camelcase/README.md
先看看测试用例,心里有个数:
type camelCase1 = CamelCase<'hello_world_with_types'>
// 预期为 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'>
// 预期为 'helloWorldWithTypes'
extends 除了表示从一个类型扩展出另外一个新类型,还能用作条件类型,其写法有点像 JS 中的三元表达式(条件 ? true 表达式 : false 表达式)
SomeType extends OtherType ? TrueType : FalseType;
意为:如果 SomeType 可以分发给 OtherType,那么返回 TrueType,否则返回 FalseType。
比如:
type Example = Dog extends Animal
? number
: string;
// number
Dog 可以分发给 Animal,属于 Animal 的子类型,Example 会得到 number 类型
infer 可以在 extends 的条件语句中推断待推断的类型,它一定是出现在条件类型中的。
比如可以利用 infer 推断某个函数的返回值类型:
type ReturnType<T> =
T extends (...args: any[]) => infer R
? R
: any;
// R 就是函数的返回值类型
利用 infer 推断某个数组每一项的类型:
type GetItem<T> =
T extends (infer R)[] ? R : T;
// R 就是数组每一项的类型
它就是对于 extends 后面未知的某个类型进行一个占位 infer R,后续就可以使用推断出来的 R 这个类型。
ts 有一些内置的字符操作类型:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting> // "HELLO, WORLD"
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// "hello, world"
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// "Hello, world"
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// "hELLO WORLD"
除了上面内置类型之外,还可以使用模板字符串
type World = 'world';
type Greeting = `hello ${World}`;
// "hello world"
type CamelCase<S> =
S extends `${infer P}_${infer T}`
? `${P}${Capitalize<T>}`
: S
type camelCase = CamelCase<'foo_bar'>
需要递归对下划线后的字符继续调用 camelCase
type CamelCase<S> =
S extends `${infer P}_${infer T}`
? `${P}${Capitalize<CamelCase<T>>}`
: S
所以还需要将其余剩余字母转换成小写。
type CamelCase<S extends string> =
S extends Lowercase<S>
? S extends `${infer P}_${infer T}`
? `${P}${Capitalize<CamelCase<T>>}`
: S
: CamelCase<Lowercase<S>>
完整代码如下:
type CamelCase<S extends string> =
S extends Lowercase<S>
? S extends `${infer P}_${infer T}`
? `${P}${Capitalize<CamelCase<T>>}`
: S
: CamelCase<Lowercase<S>>
实现了依赖的 CamelCase,现在可以来实现最终的 Camelize 了。
先看看测试用例:
type camelize = Camelize<{
some_prop: string,
prop: {
another_prop: string
},
array: [{
snake_case: string
}]
}>
// expected to be
// {
// someProp: string,
// prop: {
// anotherProp: string
// },
// array: [{
// snakeCase: string
// }]
// }
可以使用 keyof 获取某个对象类型 T 的所有 key 的集合,比如:
interface Person {
name: string;
age: number;
}
type attrs = keyof Person;
// attrs 的类型为 "name" | "age" 的联合类型
所以遍历一个对象类型 T,获取它的 key 和 value 类型可以这样写:
type traverse<T extends Object> = {
[P in keyof T]: T[P]
}
P in keyof T 表示 P 是 T 的其中一个 key,P 就是 key 的联合类型,T[P] 表示 value 的联合类型
参考上面操作,P 是 T 的某个索引,T[P] 可以表示对象 value 的联合类型,
数组的索引都是 number,所以可以用 T[number] 来表示数组 value 的联合类型
type camelize = Camelize<{
foo_bar: 'foo_bar'
}>
先根据上面遍历对象的方法,得到入参 key 和 value 对应的联合类型
type Camelize<T> = T extends Object
? {
[P in keyof T]: T[P]
}
: T
现在先将 key 转换为 camelCase,调用一开始实现的 camelCase 方法,但是直接将 P in keyof T 这一整部分传入 CameCase 类型会报错
这里需要使用 as 断言,比如断言为 string。
type Camelize<T> = T extends Object
? {
[P in keyof T as string]: T[P]
}
: T
然后再把这个 string 通过 CamelCase 转换一下,这里要联合 extends 一起使用。
type Camelize<T> = T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P]
}
: T
结果
处理了 key,我们还需要继续对 T[P] 进行处理,如果 T[P] 是对象就继续递归调用 Camelize,保证嵌套的对象都能正确转换。
type Camelize<T> = T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P] extends Object
? Camelize<T[P]>
: T[P]
}
: T
验证下结果
上面我们只处理了对象,接下来处理数组的场景。
在处理对象时,T[P] 可能是数组,所以 Camelize 的入参除了是对象,还可能是数组,需要在一开始新增判断数组的逻辑
type Camelize<T> = T extends any[]
? // 处理数组
: T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P] extends Object
? Camelize<T[P]>
: T[P]
}
: T
接着对数组中每一项都跑一遍 Camelize
type Camelize<T> = T extends any[]
? [Camelize<T[number]>]
: T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P] extends Object
? Camelize<T[P]>
: T[P]
}
: T
完整代码
type Camelize<T> = T extends any[]
? [Camelize<T[number]>]
: T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P] extends Object
? Camelize<T[P]>
: T[P]
}
: T
至此,使用 ts 实现 Camelize 的所有代码如下:
type CamelCase<S extends string> =
S extends Lowercase<S>
? S extends `${infer P}_${infer T}`
? `${P}${Capitalize<CamelCase<T>>}`
: S
: CamelCase<Lowercase<S>>
type Camelize<T extends Object | any[]> = T extends any[]
? [Camelize<T[number]>]
: T extends Object
? {
[P in keyof T as P extends string ? CamelCase<P> : P]: T[P] extends Object
? Camelize<T[P]>
: T[P]
}
: T
相信掌握了上面的知识以及完成本次实战的同学,大家完成其它的 ts 挑战也是分分钟的事。
[1]
type challenge: https://github.com/type-challenges/type-challenges/blob/main/README.zh-CN.md