类型和值

首先,让我们来搞清楚的语言和类型语言的一个重要的区分。的语言让我们可以编写在生产环境中运行的代码,并为我们的用户做有用的事情。然而,类型语言,在代码到发布到生产环境之前,就被完全删除了。它只是为了帮助 TypeScript 在代码发布之前,确保代码中不包含错误。

JavaScript 是没有类型的语言,所以,所有的 JavaScript 代码都是 value-level1 代码:

// A simple Javascript function:
function sum(a, b) {
  return a + b;
}

TypeScript 让我们在 JavaScript 中添加类型注释,并确保我们编写的 sum 函数被调用时只能传入 number 类型的参数:

// Using type annotations:
function sum(a: number, b: number): number {
  return a + b;
}

但 TypeScript 的类型系统比它强大得多。真实世界里,我们编写的代码有时需要是通用的,并且需要接收我们没法提前知道的类型。

在这种情况下,我们可以在尖括号 <A,B,...> 中定义类型参数,并使用它们给函数参数添加注释,像这样 a:A 。然后,我们可以将类型参数传递给 type-level 函数,该函数根据输入类型计算输出类型:

// Using type level programming:
function genericFunction<A, B>(a: A, b: B): DoSomething<A, B> {
  return doSomething(a, b);
}

这就是 type-level 编程! DoSomething<A,B> 是用一种特殊的编程语言编写的 type-level 函数,它与我们用于值的语言不同,但同样强大。我们称这种语言为 Type-level TypeScript

// This is a type-level function:
type DoSomething<A, B> = ...

// This is a value-level function:
const doSomething = (a, b) => ...

类型的语言

Type-level TypeScript 是一种最小的、纯函数式的语言。

该定义中的 "函数式 "一词指的是函数式编程,一个你可能早已听说过了的概念。Type-level TypeScript 是函数式的,是因为函数是这种语言的主要抽象手段。在 type-level 编程中,我们将一直使用函数。

在 type-level 层面,函数被称为泛型类型:它们接受一个或几个类型参数,并返回一个单一的类型输出。下面是一个简单的例子:一个函数接受两个类型参数,并把它们包装成一个元组类型返回:

type SomeFunction<A, B> = [A, B];
/*                ----    ------
                   ^         \
                  type        return type
               parameters

     \-------------------------/
                 ^
              Generic
*/

Type-level TypeScript 没有太多的特性。毕竟它是专门为你的代码添加类型而生! 但另一方面,它确实有足够的功能来(几乎)实现 图灵完备性 ,这意味着你可以用它解决任何复杂的问题。

以下是使用 Type-Level TypeScript 可以做到的一些事:

  • 代码分支:根据条件(相当于值级别的 if/else 关键字)执行不同的代码路径。
  • 变量赋值:声明一个变量并在表达式中使用它(相当于值级别的 var/let 关键字)。
  • 函数:可重复使用的逻辑,如我们在上一个示例中看到的那样。
  • 循环:通常通过递归来实现循环。
  • 相等检查==,类型语言的相等判断!
  • 还有更多!

以下是一些无法做到的事情:

  • 无法修改变量值:你不能在 type-Level 将变量重新赋值。
  • 没有输入/输出:你不能执行副作用,例如将某些内容打印到控制台、读取文件或在 type-Level 发出 HTTP 请求。这是幸运的:你不会希望类型系统能读取你的文件并将它们发送到某个服务器!
  • 没有高阶函数:你不能将函数传递给 type-Level TypeScript 中的另一个函数。这是 value-level 上非常常见的模式。例如,.map.filter.reduce 是高阶函数。我们无法在 type-Level 实现这些。但实际上,这种限制并没有那么糟糕,因为 type-Level 的算法通常更简单很多。

这是我们将在接下来的章节中学习的语言的简要概述。现在,让我们迎接我们的第一个挑战!

How challenges work

在每一章的末尾,你都会有有几个挑战需要解决,将你的新技能付诸实践。它们看起来是这样的:

Challenge
Reset
Loading...
  • 命名空间(namespace) 是一个不太为人所知的 TypeScript 特性,它将我们的挑战隔离在一个单独的作用域下。

  • TODO 是一个占位符。你需要用你的代码把它替换掉!

  • type res1 = ... 是你的泛型类型对某个输入类型返回的类型。你可以用鼠标悬停它来查看其当前值。

  • type test1 = Expect<Equal<res1, ...>> 是一个 type-level 的单元测试。它会报错,直到你找到正确的解决方案。

  • 我有时会使用 @ts-expect-error 注释,当我期望类型检查出错的时候。 @ts-expect-error 只在下一行类型出错的情况下才是正确的!

Challenge
Reset
Loading...

挑战

Challenge
Solution
Reset
Loading...
Challenge
Solution
Reset
Loading...
Challenge
Solution
Reset
Loading...
Challenge
Solution
Reset
Loading...

Footnotes

  1. 在计算机科学中, 人们通常使用术语来区分值和类型。“术语”这个词比“值”更令人费解,而且在我的职业生涯中很少接触过术语,所以我坚持讨论 value-level 和 type-level 的代码。我坚信“值”这个概念对于大多数开发者来说更为熟悉。