• TypeScript 泛型


    TypeScript 泛型

    在 TypeScript 中我们会使用泛型来对函数的相关类型进行约束 这里的函数 同时包含 class 的构造函数 因此 一个类的声明部分 也可以使用泛型 那么 什么是泛型? 如果通俗的理解泛型呐?

    目标:

    • 什么是泛型
    • 编译系统
    • 通俗的理解泛型
    • 泛型函数
    • 泛型类
    • 泛型接口
    • 泛型约束
    • 继承泛型类
    • 其他泛型使用的通俗解释
    • 结语

    什么是泛型

    泛型 (Generics) 是指在定义函数  接口或类的时候   不预先指定具体的类型  而在使用的时候在指定类型的一种特性
    
    • 1

    通俗的解释 泛型是类型系统中的 参数 主要作用是为了类型的重用 从上面定义可以看出 它只会用函数 接口和类中 它和 js 程序中的函数参数是两个层面的事物 (虽然意义是相同的) 因为 typescript 是静态类型系统 是在 js 进行编译时进行类型检查的系统 因此 泛型这种参数 实际上是编译过程中的运行时使用 之所以称它为参数 是因为它具备和函数参数一模一样的特性

    function increse(param) {
    	// ...
    }
    
    • 1
    • 2
    • 3

    而类型中我们如此使用泛型

    function increse<T>(param: T): T{
    	// ...
    }
    
    • 1
    • 2
    • 3

    当 param 为一个类型时 T 被赋值为这个类型 在返回值中 T 即为该类型从而进行类型检查

    编译系统

    要知道 typescript 本身的类型系统也需要编程,只不过它的编程方式很奇怪,你需要在它的程序代码中穿插 js 代码(在 ts 代码中穿插 js 代码这个说法很怪,因为我们直观的感觉是在 js 代码中夹杂了 ts 代码)。

    编程中,最重要的一种形式就是函数。在 typescript 的类型编程中,你看到函数了吗?没有。这是因为,有泛型的地方就有函数,只是函数的形式被 js 代码给割裂了。typescript 需要进行编译后得到最终产物。编译过程中要做两件事,一是在内存中运行类型编程的代码,从而形成类型检查体系,也就是说,我们能够对 js 代码进行类型检查,首先是 typescript 编译器运行 ts 编程代码后得到了一个运行时的检查系统本文来自否子戈的播客,运行这个系统,从而对穿插在其中的 js 代码进行类型断言;二是输出 js,输出过程中,编译系统已经运行完了类型编程的代码,就像 php 代码中 echo js 代码一样,php 代码已经运行了,显示出来的是 js 代码。

    从这个角度看 typescript,你或许更能理解为什么说它是 JavaScript 的超集,为什么它的编译结果是 js

    通俗的理解泛型

    既然我们理解了 ts 编译系统的逻辑 那么我们就可以把类型的编程和 js 本身的业务编程在情感上区分开 我们所讲得泛型 只存在类型编程的部分 这部分代码是 ts 的编译运行时代码

    function increase<T>(param: T): T {
    	// ...
    }
    
    • 1
    • 2
    • 3

    这段代码 如果我们把 js 代码区分开 然后用类型描述文本表示会怎么样

    // 声明函数  @type  参数  T  返回结果为 (T):T
    @type = T => (T): T
    
    // 运行函数得到一个类型  F   即类型为 (number):number
    @F = @type(number)
    
    // 要求  increase 这个函数符合  F  这种类型   也就是参数为  number  返回值为  number @@F
    function increase(param) {
    	// ...
    }
    @@end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    实际上没有 @@F 这种语法,是我编造出来的,目的是让你可以从另一个角度去看类型系统。

    当我们理解泛型是一种“参数”之后,我们可能会问:类型系统的函数在哪里?对于 js 函数而言,你可以很容易指出函数声明语句和参数,但是 ts 中,这个部分是隐藏起来的。不过,我们可以在一些特定结构中,比较容易看到类型函数的影子:

    // 声明一个泛型接口,这个写法,像极了声明一个函数,我们用描述语言来形容 @type = T => (T): T
    interface GenericTdentityFn<T> {
    	(arg: T): T;
    }
    // 这个写法,有点像一个闭包函数,在声明函数后,立即运行这个函数,描述语言:@@[T => (T): T](any)
    function identity<T>(arg: T): T {
        return arg;
    }
    
    // 使用泛型接口,像极了调用一个函数,我们用描述语言来形容 @type(number)
    let myIdentity: GenericIdentityFn<number> = identity;
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面这一段代码 我们用描述文本重写一遍

    @GenericIdentityFn = T => (T): T
    
    @@[T => (T): T] (any)
    function identify(arg) {
    	return arg
    }
    @@end
    @@GenericIdentityFn(number)
    let myIdentity = identity
    @@end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们在类型系统中声明了两个函数,分别是 @GenericIdentityFn 和 @some(匿名函数 @[T => (T): T])。虽然是两个函数,但是实际上,它们的是一模一样的,因为 typescript 是结构类型,也就是在类型检查的时候只判断结构上的每个节点类型是否相同,而不是必须保持类型变量本身的指针相同。@GenericIdentityFn 和 @some 这两个函数分别被调用,用来修饰 identify 和 myIdentify,在调用的时候,接收的参数不同,所以导致最终的类型检查规则是不同的,identify 只要保证参数和返回值的类型相同,至于具体什么类型,any。而 myIdentify 除了保证参数返回值类型相同外,还要求类型必须是 number。

    泛型函数

    目标:声明一个函数 接收一个参数 接收什么参数返回什么值

    function getValue(value: any): ant {
    	return value;
    }
    
    • 1
    • 2
    • 3
    function getString(value: string): string {
    	return value;
    }
    
    function getNumber(value: number): number {
    	return value;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    function getValue<T>(value: T): T {
    	return value;
    }
    
    getValue<string>("hello");
    getValue<number>(100);
    
    //  泛型实参可以忽略   你传递的参数的类型就是要传递的泛型的类型
    echo("hello")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    泛型类

    需求:通过类创建对象 对象的 key 属性可以是字符串可以是数值 对象的 value 属性可以是 字符串可以是数值

    {key: string, value: string}
    {key: number, value: number}
    
    • 1
    • 2
    class StringKeyValuePair {
    	constructor(public key: string, public value: string){}
    }
    
    class NumberKeyValuePair {
    	constructor(public key: number, public value: number){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    class KeyValuePair<K, V> {
    	constructor(public key: K, public value: V){}
    }
    
    • 1
    • 2
    • 3

    需求:通过类创建对象,对象中有 collection 属性,属性值为数组,数组中可以存储字符串也可以存储数值,通过索引可以获取到collection数组中的值。

    class ArrayOfNumber {
    	constructor(public collection: number[]) {}
    	get(index): number {
    		return this.collection[index];
    	}
    }
    
    class ArrayOfStrings {
    	constructor(public collection: string[]) {}
    	get(index): string {
    		return this.collection[index];
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在以上代码中 数值数组类和字符串数组类所做的事情是一样的 但由于创建的数据类型不同 所以写成了两个类 它们属于重复代码

    class ArrayOfAnything<T> {
    	constructor(public collection: T[]) {}
    	get(index): T {
    		return this.collection[index];
    	}
    }
    
    // constructor ArrayOfAnything(collection: number[]): ArrayOfAnything
    new ArrayOfAnything<number>([1,2,3]);
    // constructor ArrayOfAnything(collection: string[]): ArrayOfAnything
    new ArrayOfAnything<string>(["a", "b", "c"]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    泛型接口

    需求:创建 fetch 方法用获取数据 当获取用户数据时 fetch 方法的返回值类型为用户 当获取产品数据时 fetch 方法的返回值类型为产品 不沦是用户数据还是产品数据都要被包含在响应对象中

    interface MyUserResponse {
    	data: User;
    }
    
    interface MyProductResponse {
    	data: Product;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    interface MyResponse<T> {
    	data: T | null;
    }
    
    function fetch<T>(): MyResponse<T> {
    	return {data: null};
    }
    
    interface User {
    	username: string;
    }
    
    interface Product {
    	title: string;
    }
    
    fetch<User>().data?.username;
    fetch<Product>().data?.title;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    泛型约束

    泛型约束是指对泛型参数的范围进行约束 就是说虽然类型可以被当做参数传递 但是传递的类型不能是随意的想传什么就传什么 通过泛型约束可以限制能够传递的类型的范围

    // 限制类型 T 的范围  就是说  T  的类型要么是字符串要么是数值  其他的是不可以的
    class StringOrNumberArray<T extends string | number> {
    	constructor(public collection: T[]) {}
    	get(index: number): T {
    		return this.collection[index];
    	}
    }
    
    new StringOrNumberArray<string>(["a", "b"])
    new StringOrNumberArray<number>([100, 200])
    
    // 类型  boolean  不满足约束  string | number
    // new StringOrNumberArray([true, false]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    function echo<T extends string | number>(value: T): T {
    	return value;
    }
    echo<string>("hello")
    echo<number>(100)
    echo<boolean>(true)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    interface Person {
    	name: string;
    }
    
    function echo<T extends Person>(value: T): T{
    	return value;
    }
    echo<Person>({name: "张三"})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    class Person {
    	constructor(public name: string) {}
    	
    }
    class Custom extends Person {}
    function echo<T extends Person>(value: T): T {
    	return value;
    }
    
    echo<Person>(new Person("张三"))echo<Custom>(new Custom("李四"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    interface Printable {
    	print(): void;
    }
    
    function echo<T extends Printable>(target: T) {
    	target.print()
    }
    class Car {
    	print() {}
    }
    class Hourse {
    	print() {}
    }
    echo<Car>(new Car());
    echo<Hourse>(new Hourse());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    继承泛型类

    class Store<T> {
    	protected _objects: T[] = [];
    	add(obj: T) {
    		this._objects.push(obj);
    	}
    }
    
    interface Product {
    	name: string;
    	price: number;
    }
    const store = new Store<Product>();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    class CompressibleStore<T> extends Store<T> {}
    new CompressibleStore<Product>();
    
    
    • 1
    • 2
    • 3
    clsaa ProductStore extends Store<Product> {}
    
    • 1

    其他泛型使用的通俗解释

    接下来我们要描述一个复杂的类型:

    class Animal {
    	numLegs: number;
    }
    
    function createInstance<A extends Animal> (c: new () => A): A {
    	return new c();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们姑且不去看 new() 的部分,我们看尖括号中的 extends 语法,这里应该怎么理解呢?实际上,我们面对的问题是,在编译时, 尖括号中的内容是什么时候运行的,是之前,还是之间?

    // 到底是
    @type = (A extends Animal) => (new() => A): A
    @type(T)
    // 还是
    @type = A => (new() => A): A
    @type(T extends Animal)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为 typrscript 是静态类型系统 Animal 是不变的类 因此 可以推测其实在类的创建之前 尖括号的内容已经被运行了

    @type = (A extends Animal) => (new () => A): A
    
    • 1

    也就是说 要使用 @type(T) 产生类型 首先T要满足 Animal 结构 然后才能得到 需要的类型 如果 T 已经不满足 Animal 类的结构了 那么编译器会直接报错 而这个报错 不是类型检查阶段 而是在类型系统的创建阶段 也就是 ts 代码的运行阶段 这种情况被称为 泛型约束
    另外 类 这样的语法其实和函数参数一致

    @type = (A, B) => (A | B): SomeType
    
    
    • 1
    • 2

    我们在来看 ts 内的基础类型 Array

    @Array = any => any[]
    
    • 1

    结语

    TypeScript 中的泛型 实际上就是类型的生成函数的参数

  • 相关阅读:
    Android 视频播放
    基于V/F控制的三相逆变器MATLAB仿真模型
    Python 动态建模(1)
    idea maven构建.jar包镜像 发布到远程Linux docker 镜像
    自制操作系统日记(6):静态桌面初步
    故障预警 vs 故障分类:哪个更有意义,哪个更具挑战性?
    PowerPhotos for Mac(照片管理软件)
    我的创作纪念日
    Spring 七种事务传播性介绍
    码蹄集 - MT2142 - 万民堂大厨
  • 原文地址:https://blog.csdn.net/lhblmd/article/details/126666937