[Day12] TS:什麽!型别还有递回(recursion)的概念?用组合技实作 SnakeToCamelCase

SnakeToCamelCase

这是我们今天要聊的内容,老样的,如果你已经可以轻松看懂,欢迎直接左转去看我同事 Andy 精彩的文章 — 「前端工程师学习 DevOps 之路」。

什麽是递回函式(recursive function)

递回(recursion)是写程序时会使用到的概念,特别是在刷题的时候!?(推荐参考:Day 02 : Fibonacci 斐波那契 @ 30 天用 JavaScript 刷题刷起来!

递回函式(recursive function)简单来说就是在一个函式中呼叫它自己,举例来说:

const countDown = (num) => {
  console.log(num);
  return num > 0 ? countDown(num - 1) : 0;
};

在这个 countDown 函式中会去呼叫它自己(countDown):

recursion

recursive function 一定要有一个终止的条件,以这里来说,就是当 num <= 0 时,就不会再次呼叫自己。

TypeScript 中也能使用递回

回到 TypeScript,在 TypeScript 中的 Type Alias 和 Conditional Type 也一样可以使用递回函式的概念。

Recursive Type Aliases

来比较一下下面这两个例子:

// 没有使用 Recursive Type
type ValueOrArray<T> = T | T[];

// 使用了 Recursive Type Aliases
type ValueOrNestedArray<T> = T | ValueOrNestedArray<T>[];

先看上面的 ValueOrArray<T> 这个 Utility Type,它的概念很简单,它产生的型别可以是原本带入 <> 内的型别,或者是这个型别所建立的阵列:

type NumberArray = ValueOrArray<number>;
let numberArray: NumberArray = 0;
numberArray = [0, 1];

// ERROR: Type '[number]' is not assignable to type 'number'
numberArray = [0, [1]];

我们可以看到要满足 ValueOrArray<number> 的话,可以是一般的 number,或者是 number[],但如果是 nested 的 number[],TS 就会报错:

Recursive Type Aliases

接着我们看到第二个例子 ValueOrNestedArray<T>, 你会发现到 T | ValueOrNestedArray<T>[] 指的是它除了可以是原本带入 <> 的型别外,还在这个 Type 里呼叫了它自己,这麽做的概念就很像是:

type ValueOrNestedArray<T> = T | T[] | T[][] | T[][][] | ...;

因此,如果要满足 ValueOrNestedArray<number>,只要是 number array 都可以,即使它是 nested 的 number array:

type NestedNumberArray = ValueOrNestedArray<number>;
let nestedNumberArray: NestedNumberArray = 0;
nestedNumberArray = [0, 1];
nestedNumberArray = [0, [1]];
nestedNumberArray = [0, [1, [2]]];

// ERROR: Type 'string' is not assignable to type 'ValueOrNestedArray<number>'
nestedNumberArray = ['0', [1]];

除非带入阵列的值不是 number,否则都是能够满足 ValueOrNestedArray<number> 的,而这就是递回在 TypeScript 中的使用 — 在一个 Utility Type 中呼叫自己。

递回概念也可以使用在 Conditional Types 中,让我们回到今天最开始的那个范例。

SnakeToCamelCase 的使用方式

让我们来看一下今天的主角 SnakeToCamelCase,它的作用会像这样:

type T1 = SnakeToCamelCase<'this_is_snake_case'>; // "thisIsSnakeCase"
type T2 = SnakeToCamelCase<'This_Is_Strange_Case'>; // "thisIsStrangeCase"

type T3 = SnakeToCamelCase<'IDontKnowThis'>; // "IDontKnowThis"
  • 如果原本传入的字串型别符合 snake case 的话,它可以把原本是 snake_case 的字串型别转换成 CamelCase
  • 如果原本传入的字串型别不符合 snake case 的话,则会直接回传原本的型别回去。

SnakeToCamelCase 是修改自 ts-case-convert 中的 ToCamel 这个 Utility Type。

使用组合技写出 SnakeToCamelCase

要理解这段原始码,我们需要用到的知识包含前几天提到的:

同时,随着对 TypeScript 的知识越来越丰富,未来将会看到更复杂的 Utility Types,但原则是类似的,要先能够做出正确的断句,因此让我们先把它拆开来一一理解。

理解原始码:Generic Constraints & Conditional Types

SnakeToCamelCase

  • Generics Constraints:也就是 <T extends string>,从这里可以知道带入 SnakeToCamelCase 的型别一定要是字串型别
  • Conditional Types:在讲使用方式时有提过,如果带入的型别符合 snake case 的话,它会把它转成 camel case,否则就直接回传原本的型别回去。「如果...则...否则...」这种语句就表示用了 Conditional Types,也就是这里的 extends ... ? ... : ...,至於是怎麽判断它是不是 snake case 的话,会在下面提到。

理解原始码:infer

infer

这里是怎麽判断使用者带入的型别是否符合 snake case 呢?可以看到这里使用的条件判断是 ${...}_${...},也即是说,这个字串型别中有 _ 存在,它就可以满足 snake case。

同时可以看到这里用的了 infer,这里 infer 的作用可以帮助我们「撷取」这个 snake case 的字串,把它拆成头(Head)和尾(Tail):

infer

可以看到如果传入的是 this_is_snake_case 的话,它的 Head 会是 thisTail 会是 is_snake_case

理解原始码:Uncapitalize 和 Capitalize(Intrinsic String Manipulation Types)

接着把注意力放到当条件为 True 时,里面用了 UncapitalizeCapitalize

Intrinsic String Manipulation Types

这两个 Utility Types 称作「Intrinsic String Manipulation Types」,它们的用法就和它们的命名一样:

  • Uncapitalize<StringType>:把第一个字母变小写
  • Capitalize<StringType>:把第一个字母变大写

除了这两个之外,目前还有 Lowercase<StringType>(把所有字母变小写)和 Uppercase<StringType>(把所有字母变大写)这两个 Intrinsic String Manipulation Types。

Intrinsic String Manipulation Types 和其他的 Utility Types 有个不同的地方,Intrinsic String Manipulation Types 为了效能缘故是内建在 TS compiler 中的,因此并没有办法和其他 Utility Types 一样直接从 .d.ts 中看到它们的原始码。

现在我们知道:

  • Uncapitalize<Head> 会把 infer 撷取出来的 Head 的第一个英文字母转成小写,this 因为原本第一个字就是小写,所以不会有改变
  • Capitalize<Tail> 则会把 infer 撷取出来的 Tail 的第一个英文字母转成大写,is_snake_case 会变成 Is_snake_case

如下图所示:

Intrinsic String Manipulation Types

理解原始码:recursion

现在好像可以看出一点所以然,知道是怎麽判断传进来的型别是不是符合 snake case,也知道怎麽把它切成头尾後转成 Uncapitalize 和 Capitalize,但还少了最後一步,就是今天学到的 recursion:

recursion

这里我们在 SnakeToCamelCase 这个 Utility Types 中,又去呼叫了自己。

在学习 TypeScript 的型别操作时,有一个很好的方式是:「遇到不懂或不确定的情况,一种是带一个实际的值进去,另一种是『把它移掉,然後看看会发生什麽事』」,而要了解 recursion 帮我们做了什麽,就可以把它移掉试试看:

// 为了理解 recursion 的作用,把原本在 Capitalize<> 内的 recursive function 拿掉
type SnakeToCamelCaseWithoutRecursion<T extends string> =
  T extends `${infer Head}_${infer Tail}`
    ? `${Uncapitalize<Head>}${Capitalize<Tail>}`
    : T;

如果带入原本的范例会发现,它们都只被改了一半,也就是只有 thisIs_的部分:

type T1 = SnakeToCamelCaseWithoutRecursion<'this_is_snake_case'>; // "thisIs_snake_case"
type T2 = SnakeToCamelCaseWithoutRecursion<'This_Is_Strange_Case'>; // "thisIs_Strange_Case"

这时候就可以猜到,这里的 recursion ${Capitalize<SnakeToCamelCase<Tail>>} 做的事情,就是把前一次推论得到的 Tail 再当成参数带入 SnakeToCamelCase<Tail> 中,而这个 recursion 会一直重复执行,直到最後带入的 Tail 不符合 snake case 时终止:

recursion

而这也就是为什麽透过 SnakeToCamelCase 这个 Utility Type 能够把 snake case 的字串型别转换成 camelCase 了。

范例:FixPathSquareBrackets

在 type-fest 这个套件中,提供了一个名为 FixPathSquareBrackets 的 Utility Type,作用是把原本使用 square-bracketed syntax [] 的语法,改成使用 dot notation,具体来说就是:

`foo[0].bar` -> `foo.0.bar`

这个 Type Utility 的原始码是这样:

type FixPathSquareBrackets<Path extends string> =
  Path extends `${infer Head}[${infer Middle}]${infer Tail}`
    ? `${Head}.${Middle}${FixPathSquareBrackets<Tail>}`
    : Path;

读者们如果能够理解今天的内容,就可以试着理解 FixPathSquareBrackets 这段原始码是什麽意思!

Recursion 小技巧

  • 虽然在 TypeScript 中可以使用递回来达到强大的功能,但需要谨慎使用,因为它会让 Type Checking 所消耗的效能和时间增加,所以虽然可以用 TS 写 Fibonacci 的 Utility Type,但请不要这麽做!
  • 在学习 TypeScript 的型别操作时,有一个很好的方式是:「遇到不懂或不确定的情况,一种是带一个实际的值进去,另一种是『把它移掉,然後看看会发生什麽事』」,而要了解 recursion 帮我们做了什麽,就可以把它移掉试试看。

范例程序码

https://tsplay.dev/N5205m @ TypeScript Playground

参考


<<:  【Day 12】卑鄙源之 Hook (下) - 侦测 Hook

>>:  【从零开始的Swift开发心路历程-Day15】安装RealmSwift资料库Part1

[Day 30] 使用 Heroku 部署机器学习 API

使用 Heroku 部署机器学习 API 今日学习目标 动手部署自己的机器学习 API 使用 Her...

图的资料结构

3 图的资料结构 今天来介绍我们储存一张图的时候,几种常见的资料结构:相邻矩阵(Adjacency ...

第二天:什麽是 Gradle?以及为什麽需要它?

虽然我们或多或少听过 Gradle 这个名字,但其实在学习 Kotlin 程序语言时,好像没什麽机会...

使用 TorchServe 部署 Model

TorchServe TorchServe 是 PyTorch 提供给开发者部署 models 的工...

Day 26 - "不在办公室里"工作已快成为常态

图片来源 这标题虽然有点耸动, 但也是"软件资讯业"的业态发展趋势, 虽然新闻...