TypeScript 泛型

TypeScript 泛型

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

目标:

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

什么是泛型

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

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

function increse(param) {
	// ...
}

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

function increse<T>(param: T): T{
	// ...
}

当 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 {
	// ...
}

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

// 声明函数  @type  参数  T  返回结果为 (T):T
@type = T => (T): T

// 运行函数得到一个类型  F   即类型为 (number):number
@F = @type(number)

// 要求  increase 这个函数符合  F  这种类型   也就是参数为  number  返回值为  number @@F
function increase(param) {
	// ...
}
@@end

实际上没有 @@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;


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

@GenericIdentityFn = T => (T): T

@@[T => (T): T] (any)
function identify(arg) {
	return arg
}
@@end
@@GenericIdentityFn(number)
let myIdentity = identity
@@end

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

泛型函数

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

function getValue(value: any): ant {
	return value;
}
function getString(value: string): string {
	return value;
}

function getNumber(value: number): number {
	return value;
}
function getValue<T>(value: T): T {
	return value;
}

getValue<string>("hello");
getValue<number>(100);

//  泛型实参可以忽略   你传递的参数的类型就是要传递的泛型的类型
echo("hello")

泛型类

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

{key: string, value: string}
{key: number, value: number}
class StringKeyValuePair {
	constructor(public key: string, public value: string){}
}

class NumberKeyValuePair {
	constructor(public key: number, public value: number){}
}
class KeyValuePair<K, V> {
	constructor(public key: K, public value: V){}
}

需求:通过类创建对象,对象中有 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];
	}
}

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

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"]);

泛型接口

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

interface MyUserResponse {
	data: User;
}

interface MyProductResponse {
	data: Product;
}
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;

泛型约束

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

// 限制类型 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]);
function echo<T extends string | number>(value: T): T {
	return value;
}
echo<string>("hello")
echo<number>(100)
echo<boolean>(true)
interface Person {
	name: string;
}

function echo<T extends Person>(value: T): T{
	return value;
}
echo<Person>({name: "张三"})
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("李四"));
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());

继承泛型类

class Store<T> {
	protected _objects: T[] = [];
	add(obj: T) {
		this._objects.push(obj);
	}
}

interface Product {
	name: string;
	price: number;
}
const store = new Store<Product>();
class CompressibleStore<T> extends Store<T> {}
new CompressibleStore<Product>();

clsaa ProductStore extends Store<Product> {}

其他泛型使用的通俗解释

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

class Animal {
	numLegs: number;
}

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

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

// 到底是
@type = (A extends Animal) => (new() => A): A
@type(T)
// 还是
@type = A => (new() => A): A
@type(T extends Animal)

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

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

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

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

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

@Array = any => any[]

结语

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

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