美文网首页
TS 类型体操(热身)

TS 类型体操(热身)

作者: anOnion | 来源:发表于2024-07-15 21:41 被阅读0次

    如今,绝大多数前端开发者现在都已经接触过 Typescript。在和同僚一起学习的过程中,我发现他们虽然在使用 Typescript,但永远止步于冒号后面加上一个 type;以至于不少人号称熟练使用 ts 几年过后,依旧处于新手村阶段。因此,我计划写一份有关类型系列的文章,来帮助大家深入理解 Typescript 的原理和一些高级操作。

    type challenges

    一切开始前,先介绍 type-challenges 这个开源项目——俗称类型体操。它的提供一系列从入门到放弃的练习题来训练和提升 TypeScript 类型系统的能力。这些题目涵盖了各种常见的类型操作,如条件类型、联合类型、交叉类型、映射类型等。通过完成这些题目,你可以深入理解 TypeScript 类型系统的原理,并掌握各种类型操作的使用方法。我们本系列就从type challenges开始,借助一系列的训练题,逐步掌握 ts 类型系统的各种原理和技巧。

    warm up

    类型体操的开始是一道热身题。如下所示:

    type HelloWorld = any;
    // you should make this work
    type test = Expect<Equal<HelloWorld, string>>;
    

    这里稍微提一下, ExpectEqual 是 type-challenges 提供的辅助类型,用于测试类型是否正确。你就暂且当作 jest 里的 expecttoEqual 吧。只不过这里的 ExpectEqual 是用来比较类型(抽象)的;而 jest 的 expecttoEqual 是通常用于比较值(具体)的。

    这道题的考点是“起别名”,答案很简单:

    type HelloWorld = string;
    

    别名就是给一个类型起一个新名字,如上,给string类型起了一个HelloWorld的新名字。HelloWorldstring完全等价的,两个类型的引用可以互相复值,却毫无违和感。

    const a: string = "hello world";
    const b: HelloWorld = a;
    const c: string = b;
    

    有趣的是,我曾跟一些 Java 程序员讨论过 Typescript 中的类型别名,他们对这种概念非常陌生。或许是由于工作习惯的原因,很难理解两个不同名字的类型(或抽象)可以完全等价。同样地,我在学习 Typescript 初期,即便是多年的 js 开发,也犯过类似的错误;不过随着对类型的熟练掌握,我逐渐想明白了类型和值的关系,它们有点类似于虚数和实数的关系。在 ts 系统里, 任何定义 const val: y = x 都可以类比为复数 val = x + y*i 的形式:其中x是具体的值;而 y*i 是类型,用于约束 val 操作。在生产和工程中,虚数有点类似于辅助线,它帮助快速计算过程,但时最后的答案里虚数i却能被削掉。同理,类型别名也只是一种辅助工具,它帮助我们更好地理解类型,但在运行时,类型又不会实际产生作用。

    同样地,我们既可以在实轴上能进行数据操作,也可以在虚轴上进行特定的操作。类比虚数轴上的操作,也就是我们我们可以在纯抽象的 type 上进行类型体操了。

    complex.png

    OK,这里可能需要点数学背景。不过,现阶段不理解也无妨。每个人都可以通过特定的抽象训练,实现对类型的深入理解,并最终吃透常规的类型系统。之后的文章中,我也会举各种 type challenges 的例子,逐步讲解抽象方面的知识点。

    utility types

    热身完,我们再进一步接触一下 typescript 内置的 utility types(工具类型),它们用于处理各种常见的类型问题。这些类型就像类型系统中的“函数”,能够执行各种对类型的操作。它们为开发者提供了强大的类型处理能力,使得代码更加健壮和可维护。下面我们来看最常用的一个工具类型 Record

    Record<K, T>用于构造一个对象类型,其属性键为K,属性类型为T。例如:

    type Person = Record<"name" | "age", string>;
    
    const personA: Person = {
      // ✅
      name: "Tom",
      age: "18",
    };
    
    const personB: Person = {
      // ❌ Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.ts(2741)
      name: "Tom",
    };
    

    对于用过 Record 类型的朋友们,你们有没有想过Record是如何实现的呢?其实,在 Visual Studio Code 编辑器中,你可以通过按住 Control 键(或 Command 键,取决于你的操作系统)并点击Record,直接跳转到它的定义。

    type Record<K extends keyof any, T> = {
      [P in K]: T;
    };
    

    这段代码表明 Record 通过类型映射将属性键映射到属性值,从而创建了一个新的对象类型。至于这里的[P in K]是什么意思呢?这也是 ts 的知识点,我们会在下一篇文章中再一一介绍,这里暂不展开了。

    type mapping

    了解 ts 内置的工具类,也只是到了一个入门阶段。如果要到中高阶,不得不深入理解类型映射(type mapping)。它允许我们通过映射类型“函数”来创建新的类型。举个例子,如下所示,我们可以通过某种工具类型,将一个现有的类型 A 转换成一个新的类型 B。

    type A = {
      a: number;
      b: number;
    };
    
    // How to convert A to B?
    
    type B = {
      a: string;
      b: string;
    };
    

    初学者可能没啥思路,这里直接给出答案:

    type NumberToString<T> = {
      [K in keyof T]: string;
    };
    

    这里的keyof T又是什么意思呢?我再卖个官子,下一章再讲。

    小结

    上文中HelloWorldRecord的实现正是我们 ts 类型体操要训练的代码。这些知识主要在官方文档的types-from-types这一章节里介绍。但是这个章节的内容相对抽象,而市面上似乎也没有太多的资料介绍;因此也就有了我一开始的想法,通过一些特定的练习题,来帮助大家理解 typescript 的类型系统。

    本文作为本系列的导读篇,不再深入解释更细节的知识点。在接下来的周更系列文章中,我们会碰到各式各样的训练题,然后再一一展开。希望通过这些介绍,能够帮助大家更深入地理解 TypeScript 的类型系统,从而写出更加健壮、安全的代码。敬请期待。

    参考

    文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。

    相关文章

      网友评论

          本文标题:TS 类型体操(热身)

          本文链接:https://www.haomeiwen.com/subject/foghhjtx.html