06 - TS 泛型

泛型简介

什么是泛型?

泛型, 或者说提取了一类事物的共性特征的一种抽象。比如说,松树、柳树都是树,在程序里有3种表达:

- 接口(Interface)
- 继承(Inheritance)
- 泛型(Generics)

继承是一种强表达。
松树继承于树,松树同时也是木材。这样关系的表达,要么让松树多重集成(树、木材),要么松树<-树<-木材。
无论哪种,增加程序设计复杂度,也加强了继承关系的维护成本(或者说耦合)。这么看,关系太强,反而不好!

接口是一种方面(Aspect)描述。比如松树可以生长,那么松树是:Growable;动植物都可以进化,那么它们是Evolvable。
一个类型可以拥有多个方面的特性。

泛型(Generics)是对共性的提取(不仅仅是描述)。
class BedMaker<T> {
    make(){
}
}
const A = new BedMaker<红木>()
const B = new BedMaker<桃木>()

- 木头可以制造床,但是不是所有的木头可以制造床
- 制造床()这个方法,放到木头类中会很奇怪,因为木头不仅仅可以制造床
- 同理,让木头继承于“可以制造床”这个接口也很奇怪

奇怪的代码展示:
class 红木 implements IMakeBed{
    makeBed(){}
}
设计IMakeBed 的目标是为了拆分描述事物不同的方面(Aspect),其实还有一个更专业的词汇——关注点(Interest Point)。
拆分关注点的技巧,叫做关注点分离。如果仅仅用接口,不用泛型,那么关注点没有做到完全解耦。 划重点:泛型是一种抽象共性(本质)的编程手段,它允许将类型作为其他类型的参数(表现形式),从而分离不同关注点的实现(作用)。 Array<T> 分离的是数据可以被线性访问、存储的共性。
Stream<T>分离的是数据可以随着时间产生的共性。
Promise<T>分离的是数据可以被异步计算的特性。

初次接触泛型

// 一个identity函数是自己返回自己的函数

// 当然可以声明它是:number -> number function identity(arg: number): number { return arg; }

// 为了让identity支持更多类型可以声明它是any function identity(arg: any): any { return arg; }

// any会丢失后续的所有检查,因此可以考虑用泛型 function identity<Type>(arg: Type): Type { return arg; } let output = identity<string>("MyString")

// 不用显示的指定<>中的类型 // let output = identity("MyString") output = 100 // Error

- <>叫做钻石操作符,代表传入的类型参数

泛型类的使用

泛型类的例子。
class GenericNumber<NumType> {
    zeroValue: NumType;
    add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
    myGenericNumber.zeroValue = 0;
    // (number, number) -> number
    myGenericNumber.add = function (x, y) {
    return x + y;
    };

let stringNumeric = new GenericNumber<string>();
    stringNumeric.zeroValue = "";
    stringNumeric.add = function (x, y) {
    return x + y;
};
当然推荐将声明(Declaration)和定义(Definition)写到一起:
class GenericNumber<T> {
        zeroValue : T

        constructor(v : T){
        this.zeroValue = v
        }

        add(x : T, y : T) {
        return x + y
        }
}

泛型约束

下面的程序会报错:
function loggingIdentity<Type>(arg: Type): Type {
    console.log(arg.length);
    // Property 'length' does not exist on type 'Type'.
    return arg;
}
考虑为arg增加约束:
interface Lengthwise {
    length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
    console.log(arg.length);
    return arg;
}
小技巧keyof 操作符
可以用keyof关键字作为泛型的约束。
type Point = { x: number; y: number };
    type P = keyof Point;
    // P = "x" | "y"

如下面这个例子:
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 '"a" | "b" | "c" | "d"'.
    
为什么可以这么做?

对TS而言所有对象的key是静态的。
const a = {x : 1, y : 2}
a.z = 3 // Error
因为是静态的,所以可以用keyof 操作符求所有的key。如果一个对象的类型是any ,那么keyof就没有意义了。

实例化泛型类型

function create<Type>(c: { new (): Type }): Type {
return new c();
}

create(Foo) // Foo的实例
一个不错的例子:
class BeeKeeper {
    hasMask: boolean = true;
    }

    class ZooKeeper {
    nametag: string = "Mikle";
    }

    class Animal {
    numLegs: number = 4;
    }

    class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper();
    }

    class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper();
    }

    function createInstance<A extends Animal>(c: new () => A): A {
        return new c();
        }

        createInstance(Lion).keeper.nametag;
        createInstance(Bee).keeper.hasMask;

总结

思考:什么时候用接口?什么时候用泛型?

思考:将类型作为参数传递,并实例化有哪些应用场景?

思考:这个程序会不会报错?
function add<T>(a : T, b : T){
    return a + b
}

1. 什么时候用接口?什么时候用泛型?

接口和泛型在TypeScript中都是用来定义类型的工具,但它们的使用场景和目的有所不同:

- 使用接口:
  1. 当你需要定义一个对象的结构或形状时。例如,定义一个函数的参数对象或一个类的结构。
  2. 当你希望定义一个代码合同,让其他代码遵循这个合同时。例如,一个类实现了某个接口,就必须遵循该接口的结构。
  3. 当你希望重用某种结构,但不关心其具体类型时。

- 使用泛型:
  1. 当你希望函数、类或接口能够工作于多种类型,但又不希望丢失类型信息时。
  2. 当你希望提供类型安全,但又希望避免使用any类型时。
  3. 当你希望创建可重用的组件,这些组件可以支持多种类型的数据时。

2. 将类型作为参数传递,并实例化有哪些应用场景?

将类型作为参数传递并实例化在TypeScript中是泛型的一个重要应用。其应用场景包括但不限于:

1. 创建通用的数据结构,如数组、链表、字典、队列等。
2. 编写通用的工具函数,如用于获取数组中的最小值、最大值的函数。
3. 在开发框架或库时,为用户提供灵活的API,让用户可以传入自定义的类型。
4. 在响应式编程或者与API交互时,处理不同类型的响应数据。
5. 在某些设计模式中,如工厂模式,根据传入的类型参数实例化并返回对应的对象。

3. 这个程序会不会报错?

```tsx
function add<T>(a : T, b : T){
    return a + b
}
```
该程序会报错。因为泛型T的具体类型是未知的,所以不能保证类型T支持+操作符。除非你添加了一些约束或类型断言来确保T是一个可以进行加法操作的类型。

posted on 2022-03-08 18:50  完美前端  阅读(150)  评论(0)    收藏  举报

导航