什么是泛型
理解:泛型就是在编译期间不确定类型(广泛之意思),在调用时,由程序员指定的泛型具体指向什么类型。泛型在传统面向对象编程语言中是极为常见的,ts中当然也执行泛型。
通俗理解:泛型就是解决 类 接口 方法的重用性、以及对不特定数据类型的支持
泛型的好处
1. 函数和类可以轻松的支持多种类型,增强程序的扩展性
2. 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
3. 灵活控制类型之间的约束
泛型初识
需求:有个函数会返回任何传入它的值。
不用泛型的话,这个函数可能是下面这样:
function getData(value: string): string{
return value;//传入什么返回什么
}
但是这样,这个函数我只能传字符串类型的参数给这个函数,如果想传数字类型,布尔类型呢。这个时候可以使用 any来实现
function getData(value: any): any{
return value;//传入什么返回什么
}
any 的缺点:
如果想做到传入什么类型就返回什么类型,例如传入number就返回number,这时候就可以使用泛型
function getData<T>(value:T):T{
return value;//传入什么返回什么
}
第一种是,传入所有的参数,包含类型参数:
getData<number>(123);
getData<string>("123");
第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型:
getData(123);
getData("123");
泛型类型
一、泛型函数
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
我们还可以使用带有调用签名的对象字面量来定义泛型函数:
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;
}
1、函数泛型的注解方式;
let myIdentity: <U>(arg: U) => U = identity;
2、对象字面量的方式来定义泛型类型;
let myIdentity: {<T>(arg: T): T} = identity;
3、接口泛型的注解方式;
let myIdentity: GenericIdentityFn = identity;
想把泛型参数当作整个接口的一个参数。
这样我们就能清楚的知道使用的具体是哪个泛型类型
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
例子:
interface GenericIdentityFn{
(value1:string, value2:string):string;
}
function identity(value1:string, value2:string):string{
return value1 + value2;
}
let myIdentity: GenericIdentityFn = identity
myIdentity("name", "张三")
泛型实现上面函数
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
三、泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <> )括起泛型类型,跟在类名后面。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
使用 number 类型
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
console.log(myGenericNumber.add(1, 2))
也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "第一次 ";
stringNumeric.add = function (x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test!!!"));
示例:
普通类
class MinClass {
public list: number[] = [];
add(num: number) {
this.list.push(num);
}
min(): number {
var minNum = this.list[0];
for (var i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i];
}
}
console.log(this.list);
return minNum;
}
}
// 调用
var m = new MinClass();
m.add(3);
m.add(2);
m.add(4);
console.log(m.min());// 2
上边的只能传入数字类型,使用泛型实现上面的 类
class MinClass<T>{
public list: T[] = [];
add(num: T): void {
this.list.push(num);
}
min(): T {
var minNum = this.list[0];
for (var i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i];
}
}
return minNum;
}
}
// 调用,实例化时候要先声明参数类型<bumber
var m = new MinClass<number>();
m.add(2);
m.add(4);
m.add(3);
console.log(m.min());// 2
四、约束泛型
作某类型的一组值,并且我们知道这组值具有什么样的属性。这个时候我们想访问arg的length属性时,例如:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length) // Property 'length' does not exist on type 'T'.
return arg
}
想访问arg的length属性,但是编译器并不能证明每种类型都有length属性(比如说 number 类型、boolean 类型),所以就报错了。
这种情况可以使用泛型约束了
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
const arr = loggingIdentity([1, 2, 3])
上面代码中约束了传入的参数需要是任意类型的数组,但 Object,String类型都是有length属性的,这时候就不满足这种场景了。这时候需要对泛型进行约束,允许这个函数传入包含length属性的变量(约束泛型):
interface Lengthwise {
length: number
}
// type Lengthwise = {
// length: number
// }
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length)
return arg
}
const len01 = loggingIdentity('abc') // 3
const len02 = loggingIdentity({ length: 12 }) // 12
const len03 = loggingIdentity([1, 2, 3]) // 3
上面代码中,定义了一个接口 Lengthwise ,在函数 loggingIdentity 的泛型中使用 extends 关键字继承了Lengthwise 接口,这样调用函数 loggingIdentity 时传的参数必须要有 length 这个属性
索引类型(keyof)
官方定义:keyof该操作符可以用于获取某种类型的所有键,其返回类型是联合类型(keyof的应用多第三方库的源码中常见)
声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象obj上,因此我们需要在这两个类型之间使用约束。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
js中的Object.keys大家肯定都用过, 获取对象的键值, ts中的keyof和他类似, 可以用来获取对象类型的键值:
type A = keyof {a:1, b:'123'} // 'a'|'b'
type B = keyof [1, 2] // '1'|'2'|'push'... 获取到内容的同时, 还得到了Array原型上的方法和属性(实战中暂时没遇到这种需求, 了解即可)
可以获得键值, 也可以获取对象类型的值的类型:
type C = A['a'] // 等于type C = 1;
let c:C = 2 // 错误, 值只能是1
多重约束&交叉类型
示例1
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable
同时是Person
和Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
示例2
interface Sentence {
content: string;
title: string;
}
interface Music {
url: string;
}
class Test<T extends Sentence & Music>{
props: T
constructor(public arg: T) {
this.props = arg
}
info() {
return {
url: this.props.url,
content: this.props.content,
title: this.props.title
}
}
}
let a = new Test({
title: "泛型",
content: "xasdjoasdoijasiodhoia",
url: "aaaaaaaaaaaa"
})
console.log(a);
在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
1、参数c的注解是一个对象,可以把它看成一个接口;
2、对象里面的属性是一个方法( new() ),方法的返回值是 (T)
可以写成 function create<T>(c: new() => T): T { return new c(); }
3、有一个 new 方法说明这个方法是个构造方法,那么 T 就代表构造方法的实例
4、返回的是实例 T,所以需要调用 new方法来获取实例
function create<T>(c: {new(): T; }): T {
return new c();
}
不要乱用泛型
泛型主要是为了约束, 或者说缩小类型范围, 如果不能约束功能, 就代表不需要用泛型:
function convert<T>(input:T[]):number{
return input.length;
}
这样用泛型就没有什么意义了, 和any类型没有什么区别.
网友评论