使用泛型,我们可以轻松地将那些输入重复的代码,构建为可复用的组件,这给予了开发者创造灵活、可重用代码的能力。通俗来讲:泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。那么此时的参数类型就是一个变量,通常用大写字母 T 来表示,当然你也可以使用其他字符,如:U、K等。
function generic() {}
interface Generic {}
class Generic {}
之所以使用泛型,是因为它帮助我们为不同类型的输入,复用相同的代码。
比如写一个最简单的函数,这个函数会返回任何传入它的值。如果传入的是 number 类型:
//传入number
function identity(arg: number): number {
return arg
}
//传入string
function identity(arg: string): string {
return arg
}
//通过泛型,可以把两个函数统一起来:
function identity(arg:T):T{
return arg;
}
泛型函数可以定义多个类型参数
function extend(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
}
代码解释: 这个函数用来合并两个对象,具体实现暂且不去管它,这里只需要关注泛型多个类型参数的使用方式,其语法为通过逗号分隔
函数参数可以定义默认值,泛型参数同样可以定义默认类型:
function min(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(arg: T): T {
return arg
}
let myIdentity: (arg: T) => T = identity
同样的等号左侧的 (arg: T) => T 即为泛型类型,它还有另一种带有调用签名的对象字面量书写方式:{ (arg: T): T }:
function identity(arg: T): T {
return arg
}
let myIdentity: { (arg: T): T } = identity
通过上面的内容可以书写我们的第一个泛型接口
interface GenericIdentityFn {
(arg: T): T
}
function identity(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
//具体使用我们可以把泛型参数当成接口的一个参数,我们可以把泛型参数提前到接口名上,这样我们能清楚的知道使用的具体是哪个泛型类型:
let myIdentity: GenericIdentityFn = 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进行操作)这个时候我们就可以通过泛型来更改内容
// 类名后加上
class MinClass {
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()
m.add('hello')
m.add('world')
m.add('generic')
console.log(m.min()) // generic
通过extends来实现泛型约束
如果我们很明确传入的泛型参数是什么类型,或者明确想要操作的某类型的值具有什么属性,那么就需要对泛型进行约束。通过两个例子来说明:
interface user {
userName: string;
}
function info(user: T): string {
return 'cy' + user.userName;
}
console.log(info({ userName: '123' }));
这里面,我一直以为exteds是继承user 然后使用的时候不传也可以进行使用。现在我实战了一下,这里面确实是约束T类型的。如果这样必须传userName
第二个例子
type Args = number | string;
class MinClass {}
const m = new MinClass(); //success
const m = new MinClass(); //error,T is number or string
通过
interface name {
name: string;
}
interface age {
age: number;
}
class Classic {
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此类实际应用到了泛型
继续分析ParadigmBlockFormConfigBase做了什么
//父级类做了哪些内容??? 很简单作为一个抽象类,指示单纯的提供接收类型,什么也没做 然后继承了Form的configbase
export abstract class ParadigmBlockFormConfigBase<
TValue extends IParadigmBlockValue = IParadigmBlockValue,
TConfig extends IParadigmBlockInputConfig = IParadigmBlockInputConfig
> extends ParadigmFormConfigBase {}
以上内容详解:这里应用到了 上述的基础理论,例如
<
TValue extends IParadigmBlockValue = IParadigmBlockValue,
TConfig extends IParadigmBlockInputConfig = IParadigmBlockInputConfig
>
export interface IParadigmBlockValue {
feedback: IFeedbackValue;
stat: IBlockStatValue;
}
export type IParadigmBlockInputConfig = Omit<
Record,
'feedback' | 'stat'
> & {
feedback: FeedbackInputConfig;
stat: BlockStatInputConfig;
};
/**
*详细解释 TValue需要IParadigmBlockValue的约束 也就是说传过来的内容必须有IParadigmBlockValue的值feedback和stat否则就会报错 默认值为IParadigmBlockValue的类型
*同理 TConfig的值需要类型IParadigmBlockInputConfig的约束,再详细解释一下类型*IParadigmBlockInputConfig:类型需要传递TValue 并且需要IParadigmBlockValue的约束。这里使用了* *Omit那么针对这个是做什么?这里解释放不下 放到了代码的下解释中。这里使用了Record也在下面解释
*了解了基础知识 我们就明白了上面的具体含义。但是这里因为逻辑功能和组件有所关联。其实剔除在连接上
* 相同的'feedback' | 'stat' 是为了共用类型,但是类型需要从value变成config
**/
Omit是TypeScript3.5新增的一个辅助类型,它的作用主要是:以一个类型为基础支持剔除某些属性,然后返回一个新类型。
type Person ={
name:string;
age:string;
location:string;
}
type PersonWithoutLocation = Omit
//PersonWithoutLocation equal to QuantumPerson
type QuantumPerson = {
name: string;
age: string;
};
在TS中,类似数组,字符串,等接口是非常常见的。但是如果想定义一个对象的key和value类型改怎么做。这时候需要TS和Record.看了下面的代码就一目了然了
interface PageInfo{
title:string;
}
type Page = "home" | "about" | "contact";
const nav:Record = {
about:{title:"about"},
contact:{title:"contact"},
home:{title:"home"}
}
//这就很好理解了Record后面的泛型是对象键和值的类型
假如我有三个属性分别是abc,属性值必须是数字,这个时候我们可以这么写
type keys = 'a' | 'b' | 'c';
const result:Record = {
a:1,
b:2,
c:3
}
这个代码里面也用到了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(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 {
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
{
...
protected abstract _createInputConfig(): TInputConfig;
protected abstract _createDefaultValue(): TValue;
}
对此回归最初操作在页面中需要执行两个构造函数的方法,在这个过程中我们学习到了 如果一个抽象类继承另一个抽象类的时候是不需要实现抽象方法的,只有在不是抽象类的时候继承了抽象类才需要实现抽象类中的方法