三、TypeScript 泛型

什么是泛型

理解:泛型就是在编译期间不确定类型(广泛之意思),在调用时,由程序员指定的泛型具体指向什么类型。泛型在传统面向对象编程语言中是极为常见的,ts中当然也执行泛型。

通俗理解:泛型就是解决 类 接口 方法的重用性、以及对不特定数据类型的支持

泛型的好处

1. 函数和类可以轻松的支持多种类型,增强程序的扩展性

2. 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性

3. 灵活控制类型之间的约束


泛型初识
需求:有个函数会返回任何传入它的值。

不用泛型的话,这个函数可能是下面这样:

function getData(value: string): string{
  return value;//传入什么返回什么
}

但是这样,这个函数我只能传字符串类型的参数给这个函数,如果想传数字类型,布尔类型呢。这个时候可以使用 any来实现

function getData(value: any): any{
  return value;//传入什么返回什么
}

any 的缺点:\color{red}{一个变量设置类型为any后,相当于该变量关闭了TS的类型检测,一般情况下不建议使用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同时是PersonSerializableLoggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(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类型没有什么区别.

TypeScript 中提供了许多常用的泛型工具类型,这些工具类型可以帮助我们更轻松地定义通用的类型映射和转换。以下是一些常见的 TypeScript 泛型工具类型:

Partial<T>:将类型 T 的所有属性设置为可选。
Required<T>:将类型 T 的所有属性设置为必选。
Readonly<T>:将类型 T 的所有属性设置为只读。
Record<K, T>:创建一个具有指定键类型 K 和值类型 T 的对象类型。
Pick<T, K>:从类型 T 中选择部分属性,仅包含键为 K 的属性。
Omit<T, K>:从类型 T 中排除某些属性,即移除键为 K 的属性。
Exclude<T, U>:从类型 T 中排除可以赋值给 U 的类型。
Extract<T, U>:从类型 T 中提取可以赋值给 U 的类型。
NonNullable<T>:从类型 T 中剔除 null 和 undefined 类型。
ReturnType<T>:获取函数类型 T 的返回类型。
Parameters<T>:获取函数类型 T 的参数类型组成的元组类型。

泛型的使用场景
大神的泛型讲解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容