前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你完成 TypeScript Hard 难度题

手把手教你完成 TypeScript Hard 难度题

作者头像
Leecason
发布2023-12-14 10:09:51
2280
发布2023-12-14 10:09:51
举报
文章被收录于专栏:小李的前端小屋

本文将会手把手带你解锁一道 TypeScript 类型挑战题 ——《实现 Camelize 函数》。

虽然难度为【困难】,但期间会对用到的 ts 知识(类型体操基础动作)做学习和讲解,理解起来依然轻松,让你从此不再害怕类型体操。

题目

type challenge[1] 是一个 TypeScript 类型体操姿势合集。里面有「简单」「中等」「困难」「地狱」四个等级的题目。

今天我们来完成其中的一道困难级别题目:实现 Camelize 函数。

原题地址:https://github.com/type-challenges/type-challenges/blob/main/questions/01383-hard-camelize/README.md

题目如下:

实现 Camelize 类型: 将对象属性名从 蛇形命名(下划线命名) 转换为 小驼峰命名

代码语言:javascript
复制
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 自己实现一下。

JS 代码实现 camelize

代码语言:javascript
复制
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;
}

理一下逻辑:

  1. 入参是个对象或数组
  2. 如果是数组,则对每一项递归进行 camelize
  3. 如果是对象,将对象的 key 改为 camelCase,并对 value 递归进行 camelize
  4. 否则,不处理直接返回

可以看到 camelize 的实现依赖 camelCase,camelCase 来自于 lodash。

但 ts 类型里没有 lodash,因此我们也首先用 ts 类型来实现 CamelCase

TS 实现 CamelCase

该题也是 ts 类型挑战中难度为 Hard 类型的题目。

原题地址:https://github.com/type-challenges/type-challenges/blob/main/questions/00114-hard-camelcase/README.md

Test Case

先看看测试用例,心里有个数:

代码语言:javascript
复制
type camelCase1 = CamelCase<'hello_world_with_types'> 
// 预期为 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'> 
// 预期为 'helloWorldWithTypes'

预备知识

条件类型(extends 关键字)

extends 除了表示从一个类型扩展出另外一个新类型,还能用作条件类型,其写法有点像 JS 中的三元表达式(条件 ? true 表达式 : false 表达式)

代码语言:javascript
复制
SomeType extends OtherType ? TrueType : FalseType;

意为:如果 SomeType 可以分发给 OtherType,那么返回 TrueType,否则返回 FalseType。

比如:

代码语言:javascript
复制
type Example = Dog extends Animal 
  ? number 
  : string;
// number

Dog 可以分发给 Animal,属于 Animal 的子类型,Example 会得到 number 类型

条件类型中的类型推断(infer 关键字)

infer 可以在 extends 的条件语句中推断待推断的类型,它一定是出现在条件类型中的。

比如可以利用 infer 推断某个函数的返回值类型:

代码语言:javascript
复制
type ReturnType<T> = 
  T extends (...args: any[]) => infer R 
    ? R 
    : any;
// R 就是函数的返回值类型

利用 infer 推断某个数组每一项的类型:

代码语言:javascript
复制
type GetItem<T> = 
  T extends (infer R)[] ? R : T;
// R 就是数组每一项的类型

它就是对于 extends 后面未知的某个类型进行一个占位 infer R,后续就可以使用推断出来的 R 这个类型。

操作字符类型

ts 有一些内置的字符操作类型:

  • Uppercase<StringType>,把 string 都大写
代码语言:javascript
复制
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting> // "HELLO, WORLD"
  • Lowercase<StringType>,把 string 都小写
代码语言:javascript
复制
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting> 
// "hello, world"
  • Capitalize<StringType>,把 string 首字母大写
代码语言:javascript
复制
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>; 
// "Hello, world"
  • Uncapitalize<StringType>,把 string 首字母小写
代码语言:javascript
复制
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>; 
// "hELLO WORLD"

除了上面内置类型之外,还可以使用模板字符串

代码语言:javascript
复制
type World = 'world';

type Greeting = `hello ${World}`; 
// "hello world"

代码实现 CamelCase

  1. 因为待转换的字符是 snakeCase 下划线连接的,我们可以使用 infer 推断下划线前后的字符 P 和 T,并将 T 的首字母大写。
代码语言:javascript
复制
type CamelCase<S> = 
  S extends `${infer P}_${infer T}` 
    ? `${P}${Capitalize<T>}`
    : S
代码语言:javascript
复制
type camelCase = CamelCase<'foo_bar'>
  1. 但是这样还不够,因为字符串可能是多个下划线连接的

需要递归对下划线后的字符继续调用 camelCase

代码语言:javascript
复制
type CamelCase<S> = 
  S extends `${infer P}_${infer T}` 
    ? `${P}${Capitalize<CamelCase<T>>}`
    : S
  1. 我们只对字符进行了首字母大写的操作,但是如果一开始都是大写字母,该操作没有意义

所以还需要将其余剩余字母转换成小写。

代码语言:javascript
复制
type CamelCase<S extends string> = 
  S extends Lowercase<S> 
    ? S extends `${infer P}_${infer T}` 
        ? `${P}${Capitalize<CamelCase<T>>}` 
        : S
    : CamelCase<Lowercase<S>>

完整代码如下:

代码语言:javascript
复制
type CamelCase<S extends string> = 
  S extends Lowercase<S> 
    ? S extends `${infer P}_${infer T}` 
        ? `${P}${Capitalize<CamelCase<T>>}` 
        : S
    : CamelCase<Lowercase<S>>

TS 实现 Camelize

实现了依赖的 CamelCase,现在可以来实现最终的 Camelize 了。

Test Case

先看看测试用例:

代码语言:javascript
复制
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 的集合,比如:

代码语言:javascript
复制
interface Person {  
  name: string;
  age: number;
}

type attrs = keyof Person;

// attrs 的类型为 "name" | "age" 的联合类型

所以遍历一个对象类型 T,获取它的 key 和 value 类型可以这样写:

代码语言:javascript
复制
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 的联合类型

代码实现 Camelize

  1. 依然从最简单的入手,先来处理简单对象的情况,无嵌套,只有一层:
代码语言:javascript
复制
type camelize = Camelize<{
  foo_bar: 'foo_bar'
}>

先根据上面遍历对象的方法,得到入参 key 和 value 对应的联合类型

代码语言:javascript
复制
type Camelize<T> = T extends Object
  ? {
    [P in keyof T]: T[P]
  }
  : T

现在先将 key 转换为 camelCase,调用一开始实现的 camelCase 方法,但是直接将 P in keyof T 这一整部分传入 CameCase 类型会报错

这里需要使用 as 断言,比如断言为 string。

代码语言:javascript
复制
type Camelize<T> = T extends Object
  ? {
    [P in keyof T as string]: T[P]
  }
  : T

然后再把这个 string 通过 CamelCase 转换一下,这里要联合 extends 一起使用。

代码语言:javascript
复制
type Camelize<T> = T extends Object
  ? {
    [P in keyof T as P extends string ? CamelCase<P> : P]: T[P]
  }
  : T

结果

  1. 递归处理对象

处理了 key,我们还需要继续对 T[P] 进行处理,如果 T[P] 是对象就继续递归调用 Camelize,保证嵌套的对象都能正确转换。

代码语言:javascript
复制
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

验证下结果

  1. 处理数组

上面我们只处理了对象,接下来处理数组的场景。

在处理对象时,T[P] 可能是数组,所以 Camelize 的入参除了是对象,还可能是数组,需要在一开始新增判断数组的逻辑

代码语言:javascript
复制
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

代码语言:javascript
复制
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

完整代码

代码语言:javascript
复制
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 的所有代码如下:

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小李的前端小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 题目
  • JS 代码实现 camelize
  • TS 实现 CamelCase
    • Test Case
      • 预备知识
        • 条件类型(extends 关键字)
        • 条件类型中的类型推断(infer 关键字)
        • 操作字符类型
      • 代码实现 CamelCase
      • TS 实现 Camelize
        • Test Case
          • 预备知识
            • 遍历对象
            • 遍历数组
          • 代码实现 Camelize
          • 所有代码
            • 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档