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

558 阅读3分钟

image

题目

type challenge 是一个 TypeScript 类型体操姿势合集。里面有「简单」「中等」「困难」「地狱」四个等级的题目。今天我们来完成其中的一道困难级别题目:实现 Camelize 函数。

github.com/type-challe…

题目如下:

比如某个对象的属性都是下划线连接的,传入我们手写的 Camelize 类型,最终结果会把下划线转换成驼峰连接。

image

为了了解 camelize 的实现原理,我们先用 js 自己实现一下。

JS 代码实现 camelize

import { camelCase } from 'lodash';

export const isPlainObjectX = (obj) => Object.prototype.toString.call(obj) === '[object Object]';

function camelize(obj) {
  if (Array.isArray(obj)) {
    return obj.map(item => camelize(item));
  } else if (isPlainObjectX(obj)) {
    const newObj = Object.create(null);
    Object.keys(obj).forEach(key => {
      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 类型的题目。

image

Test Case

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

type camelCase1 = CamelCase<'hello_world_with_types'> // expected 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'> // expected 'helloWorldWithTypes'

预备知识

条件类型(extends 关键字)

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 关键字)

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 有一些内置的字符操作类型:

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

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

type World = 'world';

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

代码实现 CamelCase

  1. 因为待转换的字符是 snakeCase 下划线连接的,我们可以使用 infer 推断下划线前后的字符 P 和 T,并将 T 的首字母大写。
type CamelCase<S> = S extends `${infer P}_${infer T}` 
    ? `${P}${Capitalize<T>}`
    : S
type camelCase = CamelCase<'foo_bar'>

image

  1. 但是这样还不够,因为字符串可能是多个下划线连接的

image

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

type CamelCase<S> = S extends `${infer P}_${infer T}` 
    ? `${P}${Capitalize<CamelCase<T>>}`
    : S

image

  1. 我们只对字符进行了首字母大写的操作,但是如果一开始都是大写字母,该操作没有意义

image

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

type CamelCase<S extends string> = S extends Lowercase<S> 
    ? S extends `${infer P}_${infer T}` 
        ? `${P}${Capitalize<CamelCase<T>>}` 
        : S
    : CamelCase<Lowercase<S>>

image

完整代码如下:

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

先看看测试用例:

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 的联合类型

代码实现

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

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

type Camelize<T> = T extends Object
  ? {
    [P in keyof T]: T[P]
  }
  : T

image

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

image

这里需要使用 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

结果

image

  1. 递归处理对象

处理了 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

验证下结果

image

  1. 处理数组

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

在处理对象时,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 挑战也是分分钟的事。