前言
本文收录于TypeScript知识总结系列文章,欢迎指正!
代码复用是开发人员老生常谈的问题了,我们通过定义变量,使用函数或类减少代码重复编写。
在TS中我们可以把编写一个类型看成是新建一个函数,参数便是今天的主角:泛型,通过传入不同的参数(类型)控制类型的走向,达到类型复用的目的
定义
泛型类型是指一种不确定的类型,它允许开发者在定义函数、类、接口时不指定具体的类型,而是使用一个占位符类型,等到函数、类、接口被调用时再动态地传入具体类型,以便让它们适用于多种不同类型的数据。
使用泛型可以使代码更加通用、可复用、可扩展,提高代码的质量和可维护性。
基本用法
泛型使用尖括号<T>
表示,其中T
可以被任意字母替换,它代表一种类型,在使用时需要将其替换为具体的类型。
还记得之前数组的泛型写法吗?
const arr: Array<string> = ["a", "b"]
我们使用尖括号来标注泛型的实际类型
在JS中我们都会定义函数,每个函数的行为走向或许由参数决定,举个例子
type IObj = { name?: string } interface ISetKey { (obj: IObj, key: string): IObj } const setName: ISetKey = function (obj: IObj, name: string): IObj { if (!!!obj.name) obj.name = name return obj } const obj: IObj = setName({}, "张三")
上面这段代码,我实现了一个给对象设置值的函数,乍一看似乎没啥毛病,但是如果我给上述的代码增加一个age操作
type IObj1 = { age?: number } const setAge: ISetKey = function (obj: IObj1, age: number): IObj1 { if (!!!obj.age) obj.age = age return obj } const obj1: IObj1 = setAge({}, 20)
ISetKey这个接口几乎是要重新写了,它的结构也要随着改变,变成
interface ISetKey1 { (obj: IObj1, key: number): IObj1 }
通过这个小例子我们可以看到,如果我们在ISetKey中使用两个变量O和K来代表参数的两个类型,那么我们就可以使接口或者类型达到复用的效果
interface ISetKey<O, K> { (obj: O, key: K): O }
使用效果:
const setName: ISetKey<IObj, string> const setAge: ISetKey<IObj1, number>
掌握了上述的代码,就说明我们对泛型有了一个认识和入门
泛型命名约定
泛型的定义使用T或U之类的是为了表达特定的含义或约定,目的是增强代码的可读性,当然这些是对命名的俗成,泛型的写法是自由选择的,没有硬性要求或规定;下面我会列举一些常见的写法供参考
常见的T表示类型(type),U是未知(unknown),N表示数字(number),V是值(value)
此外还有K键名,A数组,R函数返回值,M映射等等
泛型&类型别名
既然泛型可以代表一种类型,我们可以将泛型类型参数作为类型别名的一部分与类型别名一起使用
type Animal<T, N> = { name: T age: N } const animal: Animal<string, number> = { name: "阿黄", age: 10 }
上面代码中我们实现了对Animal类型的动态描述,T表示name的类型,N表示age的类型
泛型&接口
接口中使用泛型和类型别名很相似,同样是在定义的接口名后面声明参数,在类型使用时动态传入具体类型,下面是数组及部分属性和函数的写法
interface MyArray<T> { [num: number]: T length: number push: (...item: T[]) => number forEach: (cb: (item: T, i: number, arr: MyArray<T>) => void) => void } const list: MyArray<string> = ["a", "b"] console.log(list.push("c")); list.forEach(console.log) console.log(list); /* 输出: 3 a 0 [ 'a', 'b', 'c' ] b 1 [ 'a', 'b', 'c' ] c 2 [ 'a', 'b', 'c' ] [ 'a', 'b', 'c' ] */
泛型&函数
泛型函数的写法和上面类似
// 函数声明 function getValue<T>(val: T): T { return val; } // 函数表达式 const getValue1 = function <T>(val: T): T { return val; } // 箭头函数 const getValue2 = <T>(val: T): T => { return val } getValue<string>("abc") getValue1<number>(123) getValue2<boolean>(true)
泛型&类
使用泛型来定义类,可以使类中的成员方法和属性更加灵活和通用,泛型类的使用方式与泛型接口类似,在类名后添加一个<T>来表示泛型类
interface IDog { name?: string likeMeat: boolean } interface ICat { name?: string likeFish: boolean } class Animal<Type>{ constructor(public animalType: Type) { } } const dog: IDog = { name: "阿黄", likeMeat: true } const cat: ICat = { name: "小橘", likeFish: true } new Animal<IDog>(dog) new Animal<ICat>(cat)
上述代码中,我们使用Type表示传入Animal类的动物类型,当传入IDog时,类的实例会限制其参数传入IDog的实现;ICat同理。
泛型默认值
在之前的文章中,我们了解过参数的可选性,函数的参数可以指定默认值。泛型的参数亦是如此,在不传递泛型类型参数时使用默认值可以替代参数。使用上面类型别名的例子
type Animal<T = string, N = number> = { name: T age: N } const animal: Animal = { name: "阿黄", age: 10 }
结语
本篇文章讲述的是泛型基础,我分别从泛型用法,泛型命名约定,以及泛型与接口、函数、类型别名、类等方面的写法,以及泛型默认值等方面介绍了泛型的基本使用,希望能够对你有帮助。
感谢你的阅读,如果你从文章中学到了知识,还请支持一下博主,有任何问题欢迎私信或评论,谢谢!