写点什么

扒官方文档学 Ts 类型编程

作者:GFE
  • 2022-11-03
    北京
  • 本文字数:6942 字

    阅读完需:约 23 分钟

扒官方文档学Ts类型编程

写作背景:󠀰

     TypeScript 作为 JavaScript 的一个超集带来了非常强大的类型系统,但作为天天泡在业务开发中的我们来说没感觉比其它面向对象的 Java,C#等语言高级了多少,最近发现吵吵着类型体操的人比较多,决定翻看了一下 TypeScript 文档来搞搞清楚这个类型有什么高级之处,接下来就详细上手学习一下 TypeScript 类型编程的强大之处吧。


重要的事情提前说:


  1. 你申明的是类型而非变量,你看到的 true、false 大多数均是类型而非 Boolean 类型的值。🕊️

  2. TypeScript 类型编程建议点击对应链接进Playground边看文章边调试代码学习。

TypeScript 类型操作:

     TypeScript 类型系统的强大之处主要体现在它允许我们通过类型来表达类型,也就是说我们可以通过现有的类型经过一系列的操作得到另一个类型(从类型创建类型),我们将通过下面表格所列举的顺序来讲解如何表达一个新的类型:


Generic Types:

     泛型在高级编程语言 Java、C#中的应用是很广泛的,泛型的引用使得我们将类型指定的声明周期延迟到实例化时进行,使得我们的程序设计达到更高的复用程度,变得更加灵活。

泛型引入:

     在 TypeScript 开发过程中我们可以显示的来标记传入参数和返回数据的类型,当需要支持传入和返回数据类型的限制相对宽泛我们可以使用 any 来表示,但这样也就丢失了 TypeScript 的强大之处(静态类型推断)。

定义固定类型的函数:

function identity(arg: number): number {  return arg;}
复制代码

定义任意类型的函数:

function identity(arg: any): any {  return arg;}
复制代码

使用泛型定义通用类型的函数:

  1. 泛型的特点就是通用;

  2. 泛型的语法:< T >,其中 T 是通配符,常见的通配符还有 K,U 等,下面代码中的 Type 也是通配符;

  3. 在下面执行 identity 时通过泛型约束了传入类型为 string,那么按函数功能返回的类型也将是 string,可以点击进入演练场验证答案;


function identity<Type>(arg: Type): Type {  return arg;}
let output = identity<string>("myString");
复制代码

使用泛型类型变量:

  1. 当我们在 identity 函数中直接读取 arg 变量的 length 属性时,编译器将会给我们抛出错误,提示 Type 并不存在一个名为 length 的属性。这是应为我们使用泛型定义的函数的重要特点就是通用,arg 在实际传入的时候就可以试任意类型,就会出现传入的变量的类型不一定存在 length 属性。

  2. 我们知道数组是肯定存在 length 属性的,下面的例子演示了约束类型为 number 但传入参数为数组但数组元素的类型为 number,可以点击进入演练场验证答案;


function loggingIdentity<Type>(arg: Type[]): number {  return arg.length;}let output = loggingIdentity<number>([1 , 2, 3]);
复制代码

泛型类型:

     在前面我们看到的都是最长将的泛型的使用,这里开始我们就要学习泛型类型了,请仔细看代码,“:”左边的是变量的申明,“:”右边是变量应的类型,一定要记住。

定义泛型函数<类型>:

  1. 泛型函数和非泛型函数一样,都是先将类型参数列出,泛型类型参数同样可以使用不同的通配符来表示,但类型变量的数量和使用方式要保持一致。

  2. 当然泛型类型定义还可以按对象字面量类型的方式编写,可以点击进入演练场验证答案;


function identity<Type>(arg: Type): Type {  return arg;}
let myIdentity1: <Type>(arg: Type) => Type = identity;// ^?let myIdentity2: <Input>(arg: Input) => Input = identity;// ^?let myIdentity3: { <Type>(arg: Type): Type } = identity;// ^?
复制代码

定义泛型接口<类型>:

可以点击进入演练场验证答案,定义泛型类和泛型接口类似,就不过多展开了。


interface GenericIdentityFn<Type> {  (arg: Type): Type;} function identity<Type>(arg: Type): Type {  return arg;} let myIdentity: GenericIdentityFn<number> = identity;//    ^?
复制代码

泛型通用约束:

     我们在最开始有提到在从 arg 参数获取 length 时报错 length 在 arg 中不存在的提示,我们当时为了可以正常读取 length 就另创建了一个函数并约定形参为数组并数组元素类型为泛型的 Type。那我们在不改变形参的情况下约束我们传入的 Type 一定包含一个 length 属性呢?这就体现出了通用约束的重要作用,在实际开发中也最为常见。


  1. 我们定义了一个接口,并给定一个 length 属性;

  2. 在尖括号中我们使用 extends 关键字来约束未来传入的 Type 一定是实现过 Lengthwise 接口的,这样我们就必定可以读取到 length 属性了,可以点击进入演练场验证答案。


interface Lengthwise {  length: number;} function loggingIdentity<Type extends Lengthwise>(arg: Type): number {  return arg.length;}
复制代码

泛型约束时使用类型参数:

  1. 在下面的示例中,我们发现在尖括号中使用到了逗号;

  2. 逗号前面:依旧是我们一直使用的 Type;

  3. 逗号右边:先给出答案,keyof Type 得到的将是 Type 属性 key 的集合,Key 将是这个集合中的其中一个,可以点击进入演练场验证答案。


type Types  = keyof {a: 1, b: 2, c: 3, d: 4 };//    ^?
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) { return obj[key];} let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type ...
复制代码

Keyof Type Operator:

     这里我们正式学习 Keyof 类型运算符,它的主要作用在上面的例子中也有提到,那么 keyof 的主要作用就是获取对象类型中属性名(键,key)的字符串或数字组成的联合(union)类型。当你想得到一个对象的 key(的字符串)组成的联合类型时就用 keyof。

Keyof 类型运算符:

     我们不在展示讲述,因为它的作用足够的简单,你可以点击进去演练场验证答案。这里你可以考虑一下,对象的 key 都可以是什么类型呢?什么类型的值可以充当对象的属性名称呢?


type Types  = keyof {a: 1, b: 2, c: 3, d: 4 };//   ^?
type Point = { x: number; y: number };type P = keyof Point;// ^?
type Arrayish = { [n: number]: unknown };type A = keyof Arrayish;// ^?
type Mapish = { [k: string]: boolean };type M = keyof Mapish;// ^?
复制代码

Typeof Type Operator:

     刚学完 keyof 操作符,这里就学习一个 typeof 操作符,typeof 在 JavaScript 中就有,我们在查看变量类型的时候就经常使用,那么在 TypeScript 里面 typeof 的作用是什么呢?当我们在申明一个类型的时候,我们可以使用 typeof 来将声明的变量、属性转为其类型。什么意思呢?使用 Typeof 来引用我们 JavaScript 世界的内容转为到类型世界的内容。

Typeof 类型运算符:

     可以使用 typeof 在类型上下文中使用它来引用变量或者属性的类型,可以点击进演练场验证答案。


let str = "hello world";
let type: typeof str;// ^?
复制代码

案例分析-【结合 ReturnType<T>】:

     在上面的入门示例中看到 typeof 似乎发挥的作用并不大,所以我们在了解作用和语法后结合其它类型运算符就可以表达更多的类型。


  1. 在下面的示例中我们需要通过 ReturnType 来得到 f 函数的返回类型,这里可以看到我们需要将 JavaScript 世界的 f 转为类型世界,所以需要使用 typeof 来引用 f;

  2. 我们得到的 P 类型将是{ x: number; y: number; },具体请点击进演练场验证答案。


function f() {  return { x: 10, y: 3 };}
type P = ReturnType<typeof f>;// ^?
复制代码

Indexed Access Types:

     索引访问类型和我们在编写 JavaScript 代码时的体验一样,也是使用中括号来传入索引值来获取内容,只不过在 JavaScript 中获取的内容是值,在 TypeScript 类型编程中获取的是类型。

索引访问类型:

     我们可以使用索引访问类型来查找另一种类型的特定属性,可以点击进演练场验证答案。


type Person = { age: number; name: string; alive: boolean };type Age = Person["age"]; // 输出类型 number//  ^?
复制代码

案例分析-【本身就是类型】:

     因为索引访问类型本身就是类型,所以支持使用联合、keyof,或其他类型,可以点击进演练场验证答案。


type Person = { name: string, age: number };
type I1 = Person["age" | "name"]; // 输出类型 string | number// ^?type I2 = Person[keyof Person]; // 输出类型 string | number | boolean// ^?type AgeOrName = "age" | "name"; type I3 = Person[AgeOrName]; // 输出类型 string | number// ^?
复制代码

案例分析-【数组元素的类型获取】:

     这里我们需要通过 number 关键字来配合将数组展平,以便捕获数组字面量的元素类型,可以进演练场验证答案。


const MyArray = [  { name: "Alice", age: 15 },  { name: "Bob", age: 23 },  { name: "Eve", age: 38 },]; type Person = typeof MyArray[number];       // 输出类型 { name: string; age: number; }//  ^?type Age = typeof MyArray[number]["age"];   // 输出类型 number//  ^?type Age2 = Person["age"];                  // 输出类型 number//  ^?type key = "age";type Age3 = Person[key];                    // 输出类型 number//  ^?
复制代码

Conditional Types:

     在我们学习编程的最开始阶段,当我们学习完如何输出 HelloWorld,定义变量、函数后,基本就到了逻辑部分,那么上来的第一个将是 IF 比较逻辑符,也是每个编程语言都必不可少的。那么在 TypeScript 类型编程中也需要进行判断,但不是使用 IF,而是使用类三元表达式,语法形式:SomeType extends OtherType ? TrueType : FalseType;

条件类型:

当 extends 左侧的 SomeType 可以分配给右侧 OtherType 时返回 TrueType 反之返回 FalseType。


  1. 在下面的例子中我们定义一个 Person 类和继承自 Person 类的 Student 类,我们知道 Student 属于 Person,但不是每一个 Person 都属于 Student,因为他/她长大了😅,可以点击进演练场验证答案。

  2. 注意,这里在提醒一下,下面代码中的 true、false 直接代表类型而非 Boolean 类型的值。


class Person {  name: string;  age: number;  constructor(name: string, age: number) {    this.name = name;    this.age = age;  }}
class Student extends Person { classes: string | number; constructor(name: string, age: number, classes: string | number) { super(name, age); this.classes = classes; }}
type Example1 = Student extends Person ? true : false;// ^?type Example2 = Person extends Student ? true : false;// ^?
复制代码

案例分析-【条件类型+泛型】:

     通过上面学习的示例同样看起来相当的鸡肋,我们还是需要结合其他的类型运算符来配合使用。


下面的代码演示了我们函数重载的常用做法,我们需要实现的功能是当我们传入参数是 number 类型时返回 IdLabel 类型,当传入参数是 string 类型时返回 NameLabel 类型,显然在函数重载时没办法做进一步的限制,并且写起来也是相当的繁琐。


interface IdLabel { id: number }interface NameLabel { name: string }
function createLable(id: number): IdLabel;function createLable(name: string): NameLabel;function createLable(idOrName: number | string): IdLabel | NameLabel { throw "unrealized"}
复制代码


下面的代码是我们编写的通用类型工具,来满足重载函数的缺陷:


type IdOrName<T extends number | string> = T extends number ? IdLabel : NameLabel;
复制代码


下面的代码使我们使用类型工具简化后的结果:


  1. 通过泛型约束形参类型:<Type extends number | string>

  2. 通过运行上面编写的条件类型工具得到合适的返回类型,可点击进演练场验证答案;


interface IdLabel { id: number }interface NameLabel { name: string }
type IdOrName<T extends number | string> = T extends number ? IdLabel : NameLabel;
function createLable<Type extends number | string>(idOrName: Type): IdOrName<Type> { throw "unrealized"}
let v1 = createLable("typescript"); // NameLabel// ^?let v2 = createLable(3.1415); // IdLabel// ^?
复制代码

案例分析-【条件类型约束】:

我们通过一个案例来演示条件类型约束,我们需要设计一个通用的类型工具,当传入的泛型 T 中包含一个 message 属性,我们就返回这个属性的类型,如果不包含则返回 never;


下面是我们编写的通用类型工具:


  1. 使用条件类型来判断当 T 中存在一个 message 可以分配给右侧则返回通过索引访问类型取出(T["message"])的类型;

  2. 当 T 不存在可以分配给右侧一个 message 时返回 never;


type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
复制代码


下面是验证的完整示例,可以进演练场验证答案。


type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; interface Email {  message: string;} interface Dog {  bark(): void;} type EmailMessageContents = MessageOf<Email>;//  ^? type DogMessageContents = MessageOf<Dog>;//  ^?
复制代码

案例分析-【展平数组得到元素类型】:

     在这个案例中我们希望传入的 T 是一个任意类型的数组,输出的是这个数组元素的类型,这里会用到一个特殊的索引访问“T[number]”,可以进演练场验证答案。


type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // string// ^?type Num = Flatten<number>; // number// ^?
复制代码

案例分析-【在条件类型中如何推断】:

     在上一个案例中我们使用 T[number]来得到数组元素的类型,这里我们将介绍一个新的关键字 infer,它可以方便我们在条件类型中推断出元素的类型,可以进演练场验证答案。


type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type Str = Flatten<string[]>; // string// ^?type Num = Flatten<number>; // number// ^?
复制代码


下面这个案例是推断提取返回值的类型,可以进演练场验证答案。


type GetReturnType<Type> = Type extends () => infer Return ? Return : never;type Num = GetReturnType<() => number>;         // number//  ^?type Str = GetReturnType<() => string>;         // string//  ^?type Bools = GetReturnType<() => boolean[]>;    // boolean[]//  ^?
复制代码

案例分析-【分布式条件类型】:

     分布式条件类型(Distributive Conditional Types)来自软件翻译结果,这里我更愿意理解为“分别”,因为分布式常在服务端出现,如分布式部署,简单的理解就是将同一个分别部署到不同的机器上。那么 分布式条件类型指的是传入的类型为联合类型时,则条件类型会分别应用于该联合的每个成员。


例如下面这个是我们的类型工具,当我们传入的 Type 是一个联合类型时,我们将得到一个数组联合类型,且每个数组的类型分别对应 Type 联合类型的每一个类型。


type ToArray<Type> = Type extends any ? Type[] : never;
复制代码


下面是验证代码,可以进演练场验证答案:


type ToArray<Type> = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray<string | number>;//  ^?
复制代码


当你运行上面的代码后得到的结果正如分布式条件类型的定义那样,传入的是 string 和 number 的联合类型,那么返回的将是 string[]和 number[]的联合类型。这是分布式条件类型的默认行为,但我们如何表示返回的结果需要是 string 或 number 类型的一个数组呢?这时候就需要使用中括号将 extends 两侧的类型进行包裹来避免默认的行为,可以进演练场验证答案。


type ToArray<Type> = [Type] extends [any] ? Type[] : never;type StrArrOrNumArr = ToArray<string | number>;//  ^?
复制代码

写在最后:

     在这一篇中我们通过 20 份代码片段学习了 TypeScript 类型编程的前 5 大关键内容,在这里还是建议各位伙伴可以点击对应链接进入在线 IED 以边看代码边看文章学习。TypeScript 类型编程的学习就和我们初学任何一种编程语言一样,将基础的语法灵活学习后才能在实战中运用自如。这里推荐一个在 Github 上的开源项目type-challenges,点赞高达 15k 之多,由 Vue3 的其中一位贡献者创建的学习和查阅 TypeScript 类型编程的项目感兴趣的伙伴也可以 Fork 一份自己做做看。剩下的 Mapped Types 和 Template Literal Types 在类型编程中也是很重要的两块内容,我们将单独再写一篇来详细讲解,各位尽情期待吧~


说明:


  1. 文中的大量案例沿用了官方文档的示例,同样可以参考官方文档,不足之处还请指正;

  2. 由于平台对外链限制,建议访问原文获得更好的阅读体验。

团队介绍

     高灯科技交易合规前端团队(GFE), 隶属于高灯科技(北京)交易合规业务事业线研发部,是一个富有激情、充满创造力、坚持技术驱动全面成长的团队, 团队平均年龄 27 岁,有在各自领域深耕多年的大牛, 也有刚刚毕业的小牛, 我们在工程化、编码质量、性能监控、微服务、交互体验等方向积极进行探索, 追求技术驱动产品落地的宗旨,打造完善的前端技术体系。


  • 愿景: 成为最值得信任、最有影响力的前端团队

  • 使命: 坚持客户体验第一, 为业务创造更多可能性

  • 文化: 勇于承担、深入业务、群策群力、简单开放


Github:github.com/gfe-team


团队邮箱:gfe@goldentec.com


作者:GFE-小鑫同学


著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发布于: 刚刚阅读数: 3
用户头像

GFE

关注

一起成长、一起分享、一起创造价值 2020-04-09 加入

💻 努力打造这块领土,构建完整良好的知识体系 🏠 保持对技术的热爱,保持初心,保持丰富的想象力 🏷️ 标签 | 热爱技术 钻研探索 有责任心 前端领域 📥 邮箱 | gfe@goldentec.com

评论

发布
暂无评论
扒官方文档学Ts类型编程_typescript_GFE_InfoQ写作社区