Ts基于泛型`<T>`的进阶使用

前言

模模糊糊使用Ts进行开发一年多了,在这一年的使用中个人常常把Ts当作一门简单的工具作为使用。直到这段时间,挖掘了Ts类型编程更为深入的部分,才知道自己已经停滞不前了。

通过这篇文章,你将会

  1. 深入理解泛型
  2. 认识类型断言

泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在TypeScript中泛型就承担着代替未来传入类型的责任,也可以把他看作一个能够作为类型参数变量。

泛型之Hello World

下面来创建第一个使用泛型的例子:identity函数。 并且这个函数会返回任何传入它的值。

如果不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

如上,如果使用基本变量的话。那么只能规定一种变量来作为值类型。

或者,我们使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,但是这样就不符合我们的本意:传入的类型与返回的类型应该是相同的。
比如我们传入一个数字,我们只知道任何类型的值都有可能被返回,而不能确定这个值是不是数字。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
    return arg;
}

我们给identity函数添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),相当于给T这个变量赋值以后他就是一个常量值了。之后我们就可以使用这个类型,比如我们再次使用了 T当做返回值类型。

通过使用变量T,我们就成功的规定了入出类型值统一的函数。

在这个函数中,我们就变量T,就称为了泛型

我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量

接着identity泛型函数的例子,假设此时我们想打印出arg的长度:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

如果这么直接打印的话,犹豫我们此处定义的泛型T类型实际等于任意类型。编译器不能保证任意类型都具有length属性,例如number类型,所以就报错了。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

此处的T作为一个变量,当做类型的一部分使用,而不是整个类型,增加了灵活性。

泛型约束

loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

function getProperty(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

高级类型

高级类型意在提供组合式类型以及更明确所需类型范围,其中包含

  • 交叉类型
  • 联合类型
  • 类型保护
    • 类型断言
    • 类型守卫
  • 条件类型

交叉类型

交叉类型是将多个类型合并为一个类型的类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

现有Animal类型:

type Animal = {
    name:string;
    age:number;
}

需要创建一个新的类型Dog具有新的类型并具备Animal类型的属性,此时就用到了我们的交叉类型,符号为&。

type Dog = {
    loveHuman:boolean
} & Animal

与运算符号&&类似,此时的Dog类型拥有了它特有的loveHuman属性也拥有了Animal中name以及age属性。

联合类型

联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 numberstring,或 boolean

function setObject(key:string,value:string | number){
    if(key == 'age'){
        return value -2
    }
    if(key == 'name'){
        return value 
    }
}

如上函数中我们通过联合类型表示函数所需的value参数为字符串或数字类型,对应设置了不同的返回。

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B,我们能够 确定的是它包含了 A B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 pet.fly()就出错了。

类型保护

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为 Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

为了让这段代码工作,我们要使用类型断言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}
用户自定义的类型保护(类型守卫)
is关键字

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet的类型的话就好了。

TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

注意TypeScript不仅知道在 if分支里 petFish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型。

in 关键字

其中用户可自定义的类型保护,除了使用is关键字外还有in 关键字

使用 in 关键字,我们可以进一步收窄类型(Type Narrowing),思考下面这个例子,要如何将 " A | B " 的联合类型缩小到"A"?

class A {
  public a() {}

  public useA() {
    return "A";
  }
}

class B {
  public b() {}

  public useB() {
    return "B";
  }
}

首先联想下 for...in 循环,它遍历对象的属性名,而 in 关键字也是一样,它能够判断一个属性是否为对象所拥有:

function useIt(arg: A | B): void {
  'a' in arg ? arg.useA() : arg.useB();
}

如果参数中存在a属性,由于A、B两个类型的交集并不包含a,所以这样能立刻收窄类型判断到 A 身上。

由于A、B两个类型的交集并不包含 a 这个属性,所以这里的 in 判断会精确地将类型对应收窄到三元表达式的前后。即 A 或者 B。

字面量类型
interface IBoy {
  name: "mike";
  gf: string;
}

interface IGirl {
  name: "sofia";
  bf: string;
}

function getLover(child: IBoy | IGirl): string {
  if (child.name === "mike") {
    return child.gf;
  } else {
    return child.bf;
  }
}

关于字面量类型literal types,它是对类型的进一步限制,比如你的状态码只可能是 0/1/2,那么你就可以写成 status: 0 | 1 | 2 的形式,而不是用一个 number 来表达。

  • 字符串字面量,常见如 mode: "dev" | "prod"
  • 布尔值字面量通常与其他字面量类型混用,如 open: true | "none" | "chrome"

条件类型

在编程中遇到条件判断,我们常用 If 语句与三元表达式实现

if (condition) {
  execute()
}

这种没有 else 的 If 语句,我也习惯写成:

condition ? execute() : void 0;

而 条件类型 的语法,实际上就是三元表达式,看一个最简单的例子:

T extends U ? X : Y

如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。

为什么会有条件类型?可以看到 条件类型 通常是和 泛型 一同使用的,联想到泛型的使用场景以及值得延迟推断,我想你应该明白了些什么。对于类型无法即时确定的场景,使用 条件类型 来在运行时动态的确定最终的类型(运行时可能不太准确,或者可以理解为,你提供的函数被他人使用时,根据他人使用时传入的参数来动态确定需要被满足的类型约束)。

类比到编程语句中,其实就是根据条件判断来动态的赋予变量值:

let unknownVar: string;

unknownVar = condition ? "淘系前端" : "淘宝FED";

type LiteralType<T> = T extends string ? "foo" : "bar";

条件类型理解起来其实也很直观,唯一需要有一定理解成本的就是 何时条件类型系统会收集到足够的信息来确定类型,也就是说,条件类型有时不会立刻完成判断,比如工具库提供的函数,需要用户在使用时传入参数才会完成 条件类型 的判断。

尾声

本文通过泛型入手,循序渐进地讲解相关常用的进阶写法。

本文主要参考了:

​ Ts中文官网

​ TypeScript的另一面:类型编程(2021重制版)

你可能感兴趣的:(typescript,前端)