这是我们今天要聊的内容,老样的,如果你已经可以轻松看懂,欢迎直接左转去看我同事 Andy 精彩的文章 — 「前端工程师学习 DevOps 之路」。
递回(recursion)是写程序时会使用到的概念,特别是在刷题的时候!?(推荐参考:Day 02 : Fibonacci 斐波那契 @ 30 天用 JavaScript 刷题刷起来!)
递回函式(recursive function)简单来说就是在一个函式中呼叫它自己,举例来说:
const countDown = (num) => {
console.log(num);
return num > 0 ? countDown(num - 1) : 0;
};
在这个 countDown
函式中会去呼叫它自己(countDown
):
recursive function 一定要有一个终止的条件,以这里来说,就是当 num <= 0
时,就不会再次呼叫自己。
回到 TypeScript,在 TypeScript 中的 Type Alias 和 Conditional Type 也一样可以使用递回函式的概念。
来比较一下下面这两个例子:
// 没有使用 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 就会报错:
接着我们看到第二个例子 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
,它的作用会像这样:
type T1 = SnakeToCamelCase<'this_is_snake_case'>; // "thisIsSnakeCase"
type T2 = SnakeToCamelCase<'This_Is_Strange_Case'>; // "thisIsStrangeCase"
type T3 = SnakeToCamelCase<'IDontKnowThis'>; // "IDontKnowThis"
snake_case
的字串型别转换成 CamelCase
。
SnakeToCamelCase
是修改自 ts-case-convert 中的ToCamel
这个 Utility Type。
要理解这段原始码,我们需要用到的知识包含前几天提到的:
同时,随着对 TypeScript 的知识越来越丰富,未来将会看到更复杂的 Utility Types,但原则是类似的,要先能够做出正确的断句,因此让我们先把它拆开来一一理解。
<T extends string>
,从这里可以知道带入 SnakeToCamelCase 的型别一定要是字串型别extends ... ? ... : ...
,至於是怎麽判断它是不是 snake case 的话,会在下面提到。这里是怎麽判断使用者带入的型别是否符合 snake case 呢?可以看到这里使用的条件判断是 ${...}_${...}
,也即是说,这个字串型别中有 _
存在,它就可以满足 snake case。
同时可以看到这里用的了 infer
,这里 infer 的作用可以帮助我们「撷取」这个 snake case 的字串,把它拆成头(Head)和尾(Tail):
可以看到如果传入的是 this_is_snake_case
的话,它的 Head
会是 this
,Tail
会是 is_snake_case
。
接着把注意力放到当条件为 True 时,里面用了 Uncapitalize
和 Capitalize
:
这两个 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
如下图所示:
现在好像可以看出一点所以然,知道是怎麽判断传进来的型别是不是符合 snake case,也知道怎麽把它切成头尾後转成 Uncapitalize 和 Capitalize,但还少了最後一步,就是今天学到的 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 时终止:
而这也就是为什麽透过 SnakeToCamelCase
这个 Utility Type 能够把 snake case 的字串型别转换成 camelCase 了。
在 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 这段原始码是什麽意思!
https://tsplay.dev/N5205m @ TypeScript Playground
<<: 【Day 12】卑鄙源之 Hook (下) - 侦测 Hook
>>: 【从零开始的Swift开发心路历程-Day15】安装RealmSwift资料库Part1
使用 Heroku 部署机器学习 API 今日学习目标 动手部署自己的机器学习 API 使用 Her...
3 图的资料结构 今天来介绍我们储存一张图的时候,几种常见的资料结构:相邻矩阵(Adjacency ...
虽然我们或多或少听过 Gradle 这个名字,但其实在学习 Kotlin 程序语言时,好像没什麽机会...
TorchServe TorchServe 是 PyTorch 提供给开发者部署 models 的工...
图片来源 这标题虽然有点耸动, 但也是"软件资讯业"的业态发展趋势, 虽然新闻...