使用泛型,我们可以轻松地将那些输入重复的代码,构建为可复用的组件,这给予了开发者创造灵活、可重用代码的能力。通俗来讲:泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。那么此时的参数类型就是一个变量,通常用大写字母 T 来表示,当然你也可以使用其他字符,如:U、K等。

function generic<T>() {}
interface Generic<T> {}
class Generic<T> {}
初识泛型

之所以使用泛型,是因为它帮助我们为不同类型的输入,复用相同的代码。
比如写一个最简单的函数,这个函数会返回任何传入它的值。如果传入的是 number 类型:

//传入number
function identity(arg: number): number {
    return arg
}

//传入string
function identity(arg: string): string {
    return arg
}

//通过泛型,可以把两个函数统一起来:
function identity<T>(arg:T):T{
	return arg;
}
多个类型参数

泛型函数可以定义多个类型参数

function extend<T, U>(first: T, second: U): T & U {
  for(const key in second) {
    (first as T & U)[key] = second[key] as any
  }
  return first as T & U
}

代码解释: 这个函数用来合并两个对象,具体实现暂且不去管它,这里只需要关注泛型多个类型参数的使用方式,其语法为通过逗号分隔 <T, U, K>。

泛型参数默认类型

函数参数可以定义默认值,泛型参数同样可以定义默认类型:

function min<T = number>(arr:T[]): T{
  let min = arr[0]
  arr.forEach((value)=>{
     if(value < min) {
         min = value
     }
  })
   return min
}
console.log(min([20, 6, 8n])) // 6
泛型类型与泛型接口

等号左侧的 (x: number, y: number) => string 为函数类型。

const add: (x: number, y: number) => string = function(x: number, y: number): string {
  return (x + y).toString()
}


//泛型类型
function identity<T>(arg: T): T {
  return arg
}

let myIdentity: <T>(arg: T) => T = identity

同样的等号左侧的 (arg: T) => T 即为泛型类型,它还有另一种带有调用签名的对象字面量书写方式:{ (arg: T): T }:

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

let myIdentity: { <T>(arg: T): T } = identity

通过上面的内容可以书写我们的第一个泛型接口

interface GenericIdentityFn {
  <T>(arg: T): T
}

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

let myIdentity: GenericIdentityFn = identity

//具体使用我们可以把泛型参数当成接口的一个参数,我们可以把泛型参数提前到接口名上,这样我们能清楚的知道使用的具体是哪个泛型类型:

let myIdentity: GenericIdentityFn<number> = identity

//需要注意:在使用泛型接口时,需要传入一个类型参数来指定泛型类型。实例中传入了number类型,这就锁定了之后代码里使用的类型。
泛型类

始终要记得,使用泛型是因为可以复用不同类型的代码。

class MinClass {
  public list: number[] = []
  add(num: number) {
    this.list.push(num)
  }
  min(): number {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}

更改后(这是因为需求中可能是number类型 但是有时候会对string进行操作)这个时候我们就可以通过泛型来更改内容

// 类名后加上 <T>
class MinClass<T> {
  public list: T[] = []
  add(num: T) {
    this.list.push(num)
  }
  min(): T {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}


let m = new MinClass<string>()
m.add('hello')
m.add('world')
m.add('generic')
console.log(m.min()) // generic
泛型约束

通过extends来实现泛型约束
如果我们很明确传入的泛型参数是什么类型,或者明确想要操作的某类型的值具有什么属性,那么就需要对泛型进行约束。通过两个例子来说明:

interface user {
		userName: string;
	}

	function info<T extends user>(user: T): string {
		return 'cy' + user.userName;
	}
	console.log(info({ userName: '123' }));

这里面,我一直以为exteds是继承user 然后使用的时候不传也可以进行使用。现在我实战了一下,这里面确实是约束T类型的。如果这样必须传userName

第二个例子

type Args = number | string;
	class MinClass<T extends Args> {}
  
	const m = new MinClass<number>(); //success
  	const m = new MinClass<boolean>();  //error,T is number or string
多重类型泛型约束

通过<T extends Interface1 & Interface2> 这种语法来实现多重类型的泛型约束

interface name {
		name: string;
	}
	interface age {
		age: number;
	}
	class Classic<T extends name & age> {
		private prop: T;
		constructor(arg: T) {
			this.prop = arg;
		}

		info() {
			return {
				name: this.prop.name,
				age: this.prop.age,
				test:this.prop.test //error 原因是test 不属于交叉类型中的属性,不在检测范围内
			};
		}
	}

泛型在ts中用途很广泛,可以灵活的控制类型之间的约束,提高代码复用性,增强代码可读性。但是反省在纯前端的思想中很难理解。下面我记录了工作中的代码分析。

项目实战

项目中创建单个范式试验的form的参数配置(当然具体需求可以不用管,直接上代码。然后对代码分析)
代码简洁明了:不管写几个范式,只要你继承了ParadigmBlockFormConfigBase 那你只需要完成两件事情。
● 第一确定formInput的config
● 第二确定formInput的初始值value

export class BlockFormConfig extends ParadigmBlockFormConfigBase<
	IBlockValue,
	IBlockFormInputConfig
> {
	protected _createInputConfig(): IBlockFormInputConfig {
		return {
			dimension: new SingleLevelInputConfig(blockInputPresets.dimension),
			size: createVarNumberInputConfig(undefined, blockInputPresets.size),
			delay: createVarNumberInputConfig(undefined, blockInputPresets.delay),
			feedback: createDefaultFeedbackInputConfig(),
			stat: createDefaultBlockStatInputConfig(statValues),
			method: createOrderMethodInputConfig(),
			count: createDefaultLevelCountInputConfig(),
		};
	}
	protected _createDefaultValue(): IBlockValue {
		return {
			dimension: createDefaultSingleLevelValue(2, 3),
			delay: createDefaultVarNumberValue(1),
			feedback: createDefaultFeedbackValue(),
			count: 1,
			size: createDefaultVarNumberValue(50),
			method: MethodType.Ordered,
			stat: createDefaultBlockStatValue(statValues),
		};
	}
}

针对上面代码一点点分开分析
第一继承ParadigmBlockFormConfigBase此类实际应用到了泛型<IBlockValue,IBlockFormInputConfig>
继续分析ParadigmBlockFormConfigBase做了什么

//父级类做了哪些内容??? 很简单作为一个抽象类,指示单纯的提供接收类型,什么也没做 然后继承了Form的configbase
export abstract class ParadigmBlockFormConfigBase<
	TValue extends IParadigmBlockValue = IParadigmBlockValue,
	TConfig extends IParadigmBlockInputConfig<TValue> = IParadigmBlockInputConfig<TValue>
> extends ParadigmFormConfigBase<TConfig, TValue> {}

以上内容详解:这里应用到了 上述的基础理论,例如

<
	TValue extends IParadigmBlockValue = IParadigmBlockValue,
	TConfig extends IParadigmBlockInputConfig<TValue> = IParadigmBlockInputConfig<TValue>
> 

export interface IParadigmBlockValue {
	feedback: IFeedbackValue;
	stat: IBlockStatValue;
}

export type IParadigmBlockInputConfig<TValue extends IParadigmBlockValue> = Omit<
	Record<keyof TValue, IInputConfigBase>,
	'feedback' | 'stat'
> & {
	feedback: FeedbackInputConfig;
	stat: BlockStatInputConfig;
};

/**
*详细解释 TValue需要IParadigmBlockValue的约束 也就是说传过来的内容必须有IParadigmBlockValue的值feedback和stat否则就会报错 默认值为IParadigmBlockValue的类型
*同理 TConfig的值需要类型IParadigmBlockInputConfig的约束,再详细解释一下类型*IParadigmBlockInputConfig:类型需要传递TValue 并且需要IParadigmBlockValue的约束。这里使用了* *Omit那么针对这个是做什么?这里解释放不下 放到了代码的下解释中。这里使用了Record也在下面解释
*了解了基础知识 我们就明白了上面的具体含义。但是这里因为逻辑功能和组件有所关联。其实剔除在连接上
* 相同的'feedback' | 'stat' 是为了共用类型,但是类型需要从value变成config
**/
Omit是什么?

Omit是TypeScript3.5新增的一个辅助类型,它的作用主要是:以一个类型为基础支持剔除某些属性,然后返回一个新类型。

type Person ={
	name:string;
  age:string;
  location:string;
}

type PersonWithoutLocation = Omit<Person,'location'>

//PersonWithoutLocation equal to QuantumPerson
type QuantumPerson = {
    name: string;
    age: string;
};
Record 是什么?

在TS中,类似数组,字符串,等接口是非常常见的。但是如果想定义一个对象的key和value类型改怎么做。这时候需要TS和Record.看了下面的代码就一目了然了

interface PageInfo{
	title:string;
}

type Page = "home" | "about" | "contact";

const nav:Record<Page,PageInfo> = {
	about:{title:"about"},
  contact:{title:"contact"},
  home:{title:"home"}
}
//这就很好理解了Record后面的泛型是对象键和值的类型

假如我有三个属性分别是abc,属性值必须是数字,这个时候我们可以这么写

type keys = 'a' | 'b' | 'c';
const result:Record<keys,number> = {
	a:1,
  b:2,
  c:3
}
Keyof 是什么

这个代码里面也用到了keyof他是索引类型查询操作符,假设T是一个类型,那么keyof T 产生的类型是T 的属性名称字符串字面量类型构成的联合类型。说起来有点绕口,一下子可能不是很理解。T 是数据类型并非数据本身。
看了代码可能就明白了

interface Itest{
	name:string,
  age:number,
  sex:boolean
}

type testType = keyof Itest;
//这个结果就是"name" | "age" | "sex"

真实案例:项目中的例子,根据指定key值返回对应内容

const Person = {
	name:'测试keyof',
  age:20,
  gender:'male'
}

class Student{
	constructor(private info: Person){}
  
  getInfo(key:string){
  	if(key==='name' || key === 'age' || key==='gender'){
    	return this.info[key];
    }
  }
}

//调用:
const student = new Student(Person)
const test = student.getInfo('name')
console.log(test)
/**
* 我们看到了在实例student中,如果我们调用了getInfo方法,传入key值如果不做晓燕,也就是if中的条件判断。那么很有可能返回undefined。这个时候就体现了我们的keyof
**/

用keyof改造过后的getInfo方法

class Student{
	construcotr(private info:Person){}

 getInfo<T extends Person>(key:T): Person[T]{
 		return this.info[T];
 }
}

/**
* T是泛型,通过keyof得到了Person的成员名的联合类型。这样就避免了会出现undefined的可能。
**/

第二是继承的ParadigmFormConfigBase 及在父辈的内容

// ParadigmFormConfigBase
export abstract class ParadigmFormConfigBase<
	TInputConfig extends IFormInputConfig = IFormInputConfig,
	TValue extends IFormValue = IFormValue
> extends FormConfigBase<IParadigmFormInfo, TInputConfig, TValue> {
	readonly pageType = getParadigmPageType(this.type);
	readonly peers = this.info.configManager.getFormConfigs(this.pageType);
	readonly preset = formTypePresets.get(this.type);
}

// FormConfigBase
export abstract class FormConfigBase<
	TInfo extends IFormInfo = IFormInfo,
	TInputConfig extends IFormInputConfig = IFormInputConfig,
	TValue extends IFormValue = IFormValue
> implements Readable<boolean>
{
...
	protected abstract _createInputConfig(): TInputConfig;
	protected abstract _createDefaultValue(): TValue;
}

对此回归最初操作在页面中需要执行两个构造函数的方法,在这个过程中我们学习到了 如果一个抽象类继承另一个抽象类的时候是不需要实现抽象方法的,只有在不是抽象类的时候继承了抽象类才需要实现抽象类中的方法