泛型 -- Typescript基础篇(11)

咱们在定义类型时,除了要给类型具体的规范和约束外,另外一个重要考量是可否方便的复用。typescript

为何要有泛型

假如咱们有一个foo的方法,它的参数是多个字符串,用于将全部字符串参数拼起来:函数

function foo(...list: string[]): string {
  return list.join(",");
}

foo("str1", "str2", "str3", "str4");
// result: str1,str2,str3,str4
复制代码

如今咱们有一个新的需求,传入的参数改成多个数值, 须要把全部的数值拼起来,那咱们又得定义一个bar方法:ui

function bar(...list: number[]): string {
   return list.join(",");
}

bar(1, 2, 3, 4)
// result: 1,2,3,4
复制代码

能够看出这两个方法实现代码实际上是如出一辙的,惟一的区别是参数和返回值的类型不一样。固然,为了消除这种重复,咱们能够把参数和返回值类型改成any,但这样会失去类型保护;或者咱们也能够用联合类型,结合函数重载实现,但也比较繁琐,尤为是可选类型数量不少后。this

而经过使用泛型就能很好的解决这个问题,达到类型保护和重用的目的:spa

function foo<T>(...list: T[]): string {
  return list.join(",");
}

// 只能输入string类型变量
foo<string>("str1", "str2", "str3", "str4");

// 只能输入number类型变量
foo<number>(1, 2, 3, 4)
复制代码

咱们使用T表明类型(泛型名字能够是其余任意字符串),在实际调用函数时再传入参数的类型做为约束。泛型能够应用于函数,接口,类型别名,类等常见类型。code

就算在调用foo方法时,不显式指定类型,ts也能根据实参推导出T的具体类型:cdn

generics-hinter

多个参数

泛型的参数能够是多个,各个参数使用,隔开:对象

function merge<T, U>(a: T, b: U): T & U {
  return Object.assign(a, b);
}

const result = merge({ name: "xxx" }, { age: 12 });
// result = { name: 'xxx', age: 12 }
复制代码

指定泛型类型时,要么所有指定,要么都不指定(依赖于ts自动推断)。不能只指定一部分(除非剩下的部分都有默认值):blog

function fn<T, U>(a: T, b: U) {}

// 合法
fn<string,number>("1", 1);
fn("1", 1);

// 不合法
fn<string>("1", 1);
复制代码

泛型约束

在函数内部使用泛型变量时,因为不能事先肯定它是哪一种类型,因此没法随意调用属于它的属性和方法:继承

function countLength<T>(a: T) {
	// 没法获取a.length:Property 'length' does not exist on type 'T'.
  console.log("object's length is" + a.length);
}
复制代码

这时咱们能够对T进行约束,T必须是一个有length属性的对象:

interface Lengthy {
  length: number;
}

function countLength<T extends Lengthy>(a: T) {
  console.log("object's length is" + a.length);
}
复制代码

咱们首先定义了一个Lengthy的接口,它具备length属性。而且让T继承Lengthy,保证了T是一个具备length属性的对象,实际使用时:

// 合法
countLength({ length: 10, name: "baba" });

// 不合法
// Property 'length' is missing in type '{}' but required in type 'Lengthy'
countLength({});
复制代码

咱们重构一下merge的方法。在merge方法中,咱们虽然指望传入的是两个对象,但实际并无在约束,因此实际上任何类型都是合法的。虽然Object.assign对不一样的类型的变量作了必定的额外处理保证不报错,可是实际结果可能会和预想结果不一样。咱们为泛型加上约束,保证传入的参数必须是两个对象:

function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return Object.assign(a, b);
}
复制代码

多个类型之间也能够相互约束:

function copyProperties<T extends U, U extends object>(target: T, source: U) {
  for (const key in source) {
    (target as U)[key] = source[key];
  }
  return target;
}

// 合法
copyProperties({ name: "xxx", age: 20 }, { age: 30 });

// 不合法,由于target不兼容source
copyProperties({ name: "xxx" }, { age: 30 });
复制代码

T继承U,保证了T包含U上的全部属性。

举个更复杂的例子。咱们有一个方法getProperty,它有两个参数,objkey。它的做用是获取obj[key]的值:

function getProperty<T extends object>(obj: T, key: string) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 1
getProperty(x, "m"); // undefined
复制代码

实际上咱们并无对key进行约束,咱们指望的key只能是obj中的属性。此时可使用keyof关键字:

interface Obj {
  name: string;
  age: number;
}

type Key = keyof Obj; // Key = "name" | "age"
复制代码

通过约束后的方法为:

function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 1
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'
复制代码

默认值

咱们也能够为泛型添加默认类型,这个默认类型会在没有指定类型参数,或者是ts没法从实际参数中推断出类型时生效。

function createArray<T = string>(length: number, value: T): T[] {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}
复制代码

咱们使用T = string给了T一个默认的类型。注意:

  • 若是一个泛型有默认值,那么该泛型就是可选泛型
  • 可选泛型声明必须在没有默认值的泛型以后
  • 若是泛型有约束,那么默认值必须兼容这个约束
// 可选泛型排在后面
function fn<T, U = string>(a: T, b: U) {}

interface Lengthy {
  length: number;
}

// 默认值要兼容Lengthy接口
function countLength<T extends Lengthy = string[]>(a: T) {
  console.log("object's length is" + a.length);
}
复制代码

泛型接口

泛型应用于接口:

interface Model<T> {
  data: T;
  name: string;
}

const model: Model<number> = {
  name: "model",
  data: 1,
};
复制代码

接口也能够表示泛型函数类型:

interface LogArrayFn<T> {
  (array: T[]): void;
}

// 实际声明函数时,须要指定泛型
const fn: LogArrayFn<string> = (array) => {
  console.log(array);
};

fn(["12"]);

// 另外一个形式
interface LogArrayFn {
  <T>(array: T[]): void;
}

const fn: LogArrayFn = (array) => {
  console.log(array);
};

// 在调用函数时指定泛型,或者依赖ts的自动推断
fn<string>(["12"]);
复制代码

泛型别名

类型别名也可使用泛型,其使用方法和接口十分相似:

type AliasModel<T> = {
  data: T;
  name: string;
};

const t: AliasModel<number> = {
  name: "xxx",
  data: 1,
};
复制代码

使用别名声明泛型函数:

// 声明以及使用和接口类型
type AliasLogArrayFn<T> = {
  (array: T[]): void;
};

// 或者
type AliasLogArrayFn = {
    <T>(array: T[]): void;
}; 


// 简化版
type AliasLogArrayFn<T> = (array: T[]) => void;

type AliasLogArrayFn = <T>(array: T[]) => void;

复制代码

泛型类

与泛型接口相似,泛型也能够用于类的类型定义中:

class GenericData<T> {
  data: T;
  constructor(data: T) {
    this.data = data;
  }

  logData() {
    console.log(this.data);
  }
}

// 推断出T 为 { name: string }
const data = new GenericData({ name: "xxx" });
复制代码

若是类只包含一个或多个泛型函数,也能够是:

class GenericData {
  logData<T>(data: T) {
    console.log(data);
  }
}
复制代码

泛型绑定具体类型时机

对于不一样的类型,泛型绑定具体类型的时机(或者是根据参数推断具体类型的时机)是不一样的:

  • 对于泛型函数:当调用函数时绑定
  • 对于泛型类:当实例化时绑定
  • 对于泛型接口或泛型类型别名:当使用它们时绑定

这能够解释如下现象:

// 第一种
type AliasLogArrayFn<T> = (array: T[]) => void;
const fn: AliasLogArrayFn<number> = (array) => console.log(array);
fn([1,2])

// 第二种
type AliasLogArrayFn = <T>(array: T[]) => void;
const fn: AliasLogArrayFn = (array) => console.log(array);
fn<number>([1, 2, 3]);
复制代码

第一种声明方式获得的是一个泛型别名,在使用泛型别名时就须要绑定类型,因此声明fn时须要显式指定类型,至关于声明了具体类型的方法。

第二种获得的是表明泛型函数的别名(自己不是泛型别名),因此声明fn时至关于在声明一个泛型方法,不须要绑定类型,而在调用时须要绑定类型。

相关文章
相关标签/搜索