包阅导读总结
1. `TypeScript`、`类型系统`、`集合`、`操作`、`高级特性`
2. 本文提出将 TypeScript 类型系统视为操作集合的功能性编程语言,通过将类型解析为可构造的值集合来理解其特性,包括基本数据类型的交集、联合类型、类型内省、类型映射等,并指出这种思维模式虽不完美但适用。
3.
– 提出观点
– 将 TypeScript 类型系统视为操作类型集合的纯函数语言。
– 基本概念
– 每种类型可视为能构造的字面量集合。
– 强调集合和类型不等同。
– 类型操作示例
– 交集:通过解析类型所构建的集合理解交集操作。
– 联合类型:将两个集合的并集作为新集合。
– 类型内省:可使用 extends 关键字检查子集,使用类型参数时存在特殊情况。
– 类型映射:通过示例展示如何在 TypeScript 中进行类型的转换和映射。
– 重复逻辑:使用递归来处理复杂的集合转换。
– 结论
– 将 TypeScript 视为操作集合的方式有助于理解高级特性,虽不完美但适用。
思维导图:
文章地址:https://mp.weixin.qq.com/s/MZSND–g6RcwFyPTTlUIug
文章来源:mp.weixin.qq.com
作者:Robby??Pruzan
发布时间:2024/8/13 0:24
语言:中文
总字数:3380字
预计阅读时间:14分钟
评分:87分
标签:TypeScript,类型系统,功能性编程,集合论,前端开发
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
一种不同的思考 TypeScript 类型系统的方式,即将类型视为可构造的值集合,并将 TypeScript 类型系统视为一种操作这些集合的功能性编程语言。今日前端早读课文章由 @飘飘翻译分享。
正文从这开始~~
Types -> Sets
TypeScript 类型系统可以被看作是一种操作类型(即类型上的操作)的纯函数语言。但是,对类型进行操作意味着什么呢?对我来说,我发现将类型解析为它可以构造的项的集合非常有用。这个集合包含了可以赋值给该类型的所有实际值。
【第3303期】JavaScript Set新增7个方法
然后,TypeScript 的核心语法可以用来操作任何给定集合中的项的功能,就像在普通编程语言中操作真实集合一样。
由于 TypeScript 是一个结构化类型系统,而不是命名类型系统,这个类型构造的 “集合” 有时比实际的类型定义更有用(但并非总是如此)。
如果我们将每种类型都视为它可以构造的字面量的集合 —— 实际值那么可以说,一个字符串就是所有字符排列的无限集合,而一个整数就是所有数字排列的无限集合。
一旦你开始将类型系统视为一种专门用于处理集合的高级功能编程语言,那么一些更高级的功能就会变得更加容易理解。
本文将通过 “类型是它们可以创建的集合” 这一视角,深入探讨 TypeScript 的众多特性,因为 TypeScript 是一种基于集合的函数式编程语言。
我要强调的是,集合和类型并不等同,它们是不同的概念。
分解 TypeScript 基本数据类型
交集
交集(&)是一个很好的例子,这种心智模型有助于你更好地理解操作。考虑以下示例:
type Bar = { x: number };
type Baz = { y: number };
type Foo = Bar & Baz;
我们正在对 Bar 和 Baz 进行交集操作。你可能会首先想到交集操作是以如下方式进行的:
我们会在两个对象之间寻找重叠区域,并将其作为结果。但… 如果找不到重叠区域呢?左侧(LHS)只有 x,右侧(RHS)只有 y,尽管两个数都有。那么为什么交集会导致允许以下操作的类型:
let x: Foo = { x: 2, y: 2 };
用更简单的方式来思考这个问题的方法是将类型 Bar 和 Baz 解析为它们所构建的集合,而不是它们在文本中的表现形式。
【第3030期】从集合论的角度理解 TypeScript
当我们定义一种{ y: number }
类型时,我们可以构造出一个包含至少具有属性 y 的无限对象字面量的集合,其中 y 是一个数字:
注意:请注意我说的是 “至少具有 y 属性的对象类型集合”。这就是为什么某些对象类型中存在其他属性的原因。如果你有一个类型为
{y: number}
的变量,那么即使对象中有比 y 更多的属性也无所谓,这就是为什么 TypeScript 允许这种情况。
现在我们知道如何用构造它们的集合来替换类型,那么交集的意义就更加清晰了:
联合类型
使用我们之前建立的心智模型,这很简单,我们只需将两个集合的并集作为新集合即可。
type Foo = { x: number };
type Baz = { y: number };
type Bar = Foo | Baz;
类型内省
因为 TypeScript 的维护者认为这很方便,他们在语言中内置了一些基本类型,让我们可以查看这些集合。例如,我们可以使用 extends 关键字检查一个集合是否是另一个集合的子集,并在 true/false 情况下返回一个新集合。
type IntrospectFoo = number | null | string extends number
? "number | null | string constructs a set that is a subset of number"
: "number | null | string constructs a set that is not a subset of number";
// IntrospectFoo = "number | null | string is not a subset of number"
在这里,我们正在检查 extends 关键字 的左侧集合是否是右侧集合的子集。
这非常强大,因为我们可以任意嵌套这些结构。
type Foo = null
type IntrospectFoo = Foo extends number | null
? Foo extends null
? "Foo constructs a set that is a subset of null"
: "Foo constructs a set that of number"
: "Foo constructs a set that is not a subset of number | null";
// Result = "Foo constructs a set that is a subset of null"
但当我们使用类型参数并将联合类型作为类型参数传递时,事情会变得有些奇怪。与先将联合体解析为构造集合后再进行子集检查的做法不同,TypeScript 会在使用类型参数时对联合体中的每个成员分别进行子集检查。
因此,当稍微改变前面的例子以使用类型参数时:
type IntrospectT<T> = T extends number | null
? T extends null
? "T constructs a set that is a subset of null"
: "T constructs a set that of number"
: "T constructs a set that is not a subset of number | null";
type Result = IntrospectT<number | string>;
TypeScript 将Result
转换为:
type Result = IntrospectFoo<number> | IntrospectFoo<string>;
使 Result 解析为:
“T 构造的集合仅包含数字” | “T 构造的集合中的项不包含数字或 null”
这只是因为这对大多数操作来说更加方便。但是,我们可以使用元组语法强制 TypeScript 不这样做。
type IntrospectFoo<T> = [T] extends [number | null]
? T extends null
? "T constructs a set that is a subset of null"
: "T constructs a set that of number"
: "T constructs a set that is not a subset of number | null";
type Result = IntrospectFoo<number | string>;
// Result = "T constructs a set that is not a subset of number | null"
这是因为我们不再将条件类型应用于联合体,而是将其应用于包含联合体的元组。
这个特殊情况很重要,因为它说明了一个事实,即总是将类型映射到它们立即构造的集合上的思维模型并不完美。
类型映射
在正常的编程语言中,你可以迭代一个集合(无论语言中如何实现)来创建一个新集合。例如,在 Python 中,如果你想要展平一个元组集合,你可能会这样做:
nested_set = {(1,3,5,6),(1,2,3,8), (9,10,2,1)}
flattened_set = {}
for tup in nested_set:
for integer in tup:
flattened_set.add(integer)
我们的目标是在 TypeScript 类型中完成这一操作。如果我们将Array<number>
视为包含数字数组的所有排列的集合:
我们想对每个项目中的数字进行一些转换,并将它们放入集合中。
相反,我们可以使用 TypeScript 的声明式语法来实现。例如:
type InsideArray<T> = T extends Array<infer R>
? R
: "T is not a subset of Array<unknown>";
type TheNumberInside = InsideArray<Array<number>>;
// TheNumberInside = number
这个声明执行以下操作:
检查 T 是否是Array<any>
构造的集合的子集(R 还不存在,所以我们用 any 代替)
注意 这不是基于 infer 实现的规范,这仅仅是一种通过集合心智模型来推理 infer 如何工作的方式。
从视觉上来说,我们可以把这个过程描述为:
有了这个思维模型,使用 TypeScript 中的infer
实际上就很有意义了。它会自动找到一个能够描述如何创建我们创建的集合(R`)的类型。
类型转换 —— 映射类型
我们刚刚描述了 TypeScript 如何让我们精确地检查一个集合是否看起来像某种类型,并根据该类型对它们进行映射。不过,如果我们能更具表达力地描述由一个类型构造的集合中的每个项的特征,那就更有用了。如果我们能很好地描述这个集合,我们可以创建任何我们想要的东西:
映射类型是一个很好的例子,其最初的使用非常简单,对集合中的每个元素进行映射,创建一个对象类型。
例如:
type OnlyBoolsAndNumbers = {
[key: string]: boolean | number;
};
最后一步是在我们头脑中完成的 —— 将对象类型映射回集合。
我们也可以对字符串的子集进行映射:
type SetToMapOver = "string" | "bar";
type Foo = { [K in SetToMapOver]: K };
我们在这里对["string", "bar"]
集合进行映射,创建一个对象类型 =>{string: "string", bar: "bar"}
,然后它描述了一个可以构造的集合。
我们可以对对象类型的键和值执行任意类型级别的计算:
type SetToMapOver = "string" | "bar";
type FirstChacter<T> = T extends `${infer R}${infer _}` ? R : never;
type Foo = {
[K in SetToMapOver as `IM A ${FirstChacter<K>}`]: FirstChacter<K>;
};
注意:
never
是一个空集合,集合中不存在任何值,因此具有类型never
的值永远无法被赋值任何值。
现在我们将集合["string", "bar"]
映射为新的类型:=>
{["IM A s"]: "s", ["IM A b"]: "b"}
重复逻辑
如果我们想对一个集合执行某种转换,但这个转换非常难以表示。它需要在处理下一个元素之前进行任意次数的内部计算。在运行时编程语言中,我们很容易使用循环来实现。但由于 TypeScript 的类型系统是函数式语言,我们需要使用递归来实现。
type FirstLetterUppercase<T extends string> =
T extends `${infer R}${infer RestWord}${infer RestSentence}`
? `${Uppercase<R>}${RestWord}${FirstLetterUppercase<RestSentence>}` // recurssive call
: T extends `${infer R}${infer RestWord}`
? `${Uppercase<R>}${RestWord}` // base case
: never;
type UppercaseResult = FirstLetterUppercase<"upper case me">
// UppercaseResult = "Upper Case Me"
首先…… 哈哈,这看起来可能很疯狂,但实际上只是一些复杂的代码,并不复杂。让我们用 TypeScript 运行时版本来扩展一下发生了什么:
const separateFirstWord = (t: string) => {
const [firstWord, ...restWords] = t.split(" ");
return [firstWord, restWords.join(" ")];
};
const firstLetterUppercase = (t: string): string => {
if (t.length === 0) {
// base case
return "";
}
const [firstWord, restWords] = separateFirstWord(t);
return `${firstWord[0].toUpperCase()}${firstWord.slice(1)}${firstLetterUppercase(restWords)}`; // recursive call
};
我们获取当前句子的第一个单词,并将其首字母大写,然后对其余单词也这样做,在过程中将它们连接起来。
将运行时示例与类型级示例进行比较:
我们可以使用这些能力描述任何计算(类型系统是图灵完备的)。
结论
如果你能把 TypeScript 看作是一种非常强大的操作集合的方式,并使用这些集合来强制执行严格的编译时检查,那么你很可能会开始更熟悉高级的 TypeScript 功能(如果尚未熟悉的话),从而能够更早地发现更多的 bug。
这种思维模式并非完美无缺,但在处理一些 TypeScript 的高级特性时仍然相当适用。
关于本文
译者:@飘飘
作者:@Robby Pruzan
原文:https://www.rob.directory/blog/a-different-way-to-think-about-typescript
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下 。