# 泛型

在值和类型小节我们强调 类型是一个抽象概念,表示一系列值的集合,确定了值的操作属性,那如何控制具有特定特征的类型? 本章学习泛型用于解决此类问题。

# 泛型概述

采用 <泛型符号> 申明一个泛型,其中泛型符号表示任意类型。

// 在函数申明时采用泛型符号, T 指代任意类型,参数和返回值均可采用此类型
function identity<T>(arg: T): T {
  return arg;
}

// 泛型符号名称为指示符,可以取任何合法名称
let myIdentity1: <ABC>(arg: ABC) => ABC = identity;

// 注意 `<>` 是申明在 `()` 之前!
let myIdentity2: { <T>(arg: T): T } = identity;

# 泛型函数(generic)

function createArr(length,data) {
    return [val1,val2,...]
}

一个创建数组的函数 createArr,返回的数组元素 val1,val2 为相同类型,但该类型可能为字符串,数值,或对象。如何进行限制,若采用 any[] 并不能表示数组元素类型相同这个特征。申明泛型解决此问题。

// 采用 <T> 表示创建一个泛型 T, 注意 T 指代一个类型也可换为任何字符
type CreateArr = <T>(length: number, data: T) => T[];

let createArr: CreateArr = (length: number, data: any) => {
  let arr = [];
  for (let i = 0; i < length; i++) {
    arr.push(data);
  }
  return arr;
};

// 创建长度为 10 ,元素为 1 的数组
console.log(createArr(10, 1));
// 创建长度为 10 ,元素为 'foo' 的数组
console.log(createArr(10, 'foo'));
// 创建长度为 10 ,元素为 { a: 1 } 的数组
console.log(createArr(10, { a: 1 }));

另一个示例用来限制交换元素类型必须相同

// 该函数约束只能交换相同类型的元祖数据
function swapSameType<S>(tuple: [S, S]): S[] {
  return [tuple[1], tuple[0]];
}

// 交换数值
console.log(swapSameType([1, 2]));
// 交换字符串
console.log(swapSameType(['1', '2']));
// 交换对象
console.log(swapSameType([{ a: 1 }, { b: 1 }]));

// 报错,若数组元素类型不一样会报错
console.log(swapSameType([1, '1']));

# 多个泛型参数

可以同时申明多个泛型符号 <泛型参数1,泛型参数2,...>。

// 定义泛型参数 T,U 表示交换函数可交换任意类型
// 泛型名称 T, U 可以为任意合法名称,只是一个指示符号
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

// 合法
console.log(swap([10, 'foo']));
// 合法
console.log(swap([10, 10]));
// 合法
console.log(swap([null, undefined]));

# 泛型参数默认值

有时期望泛型函数的参数支持可选,可采用给泛型参数设置默认值解决此类问题

export function getObjectKeys<Obj extends Record<string, any>,ChoiceKeys extends (keyof Obj)[] = (keyof Obj)[]>(obj: Obj,keys: ChoiceKeys) {
  return Object.keys(obj).filter(key => keys.includes(key))
}

export const a = getObjectKeys<{a: number, b: number}>({a: 1, b:2}, ['a'])

# 接口泛型

也可采用接口定义函数泛型。

interface InterfaceName {
  <T>(arg: T): T;
}

你可以将泛型申明 <T> 提升到接口名后。

interface InterfaceName<T> {
  (arg: T): T;
}

采用外部申明泛型,你可以结合类型断言,指定接口接受特定类型。

// 定义输入参数和返回值为类型 T 的泛型函数
interface InterfaceName {
  <T>(arg: T): T;
}

// 效果同上,注意泛型申明提升到了接口名后
interface InterfaceName1<T> {
  (arg: T): T;
}

// 无法采用类型断言,限制为特定类型
let val1: InterfaceName;

// 表示参数类型必须为函数且函数输入和返回值必须为 number 类型
let val2: InterfaceName1<number>;

# 类中使用泛型

也可在类中使用泛型。

# 泛型约束

采用泛型约束语法实现, 限定泛型具有某种特征,通过 extends 让泛型参数继承接口特性

interface Lengthwise {
  length: number;
}

// 泛型 T 利用 extends 扩展接口,实现类型约束
function loggingIdentity<T extends Lengthwise>(arg: T) {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]);

另一示例,限制工厂方法只能传入特定的构造函数。

class Animal {
  //  !: 表示明确类型断言,避免 TypeScript 检查到未对 numLegs 赋值报错
  numLegs!: number;
}

class Lion extends Animal {
  name!: string;
}

class Bug {}

// 采用泛型 A 限制构造器的类型必须是 Animal 及其子类
function createAnimal<A extends Animal>(C: new () => A): A {
  return new C();
}

let a1 = createAnimal(Animal);
let l1 = createAnimal(Lion);

// 由于 Bug 不继承自 Animal,此处检查会报错
let p1 = createAnimal(Bug);

# 总结

本节我们学习了修饰类型的类型泛型。

  1. 利用 <T> 的方式申明泛型符号
  2. 函数中泛型申明放在 () 之前,可同时定义多个泛型符号
  3. 采用 interface 接口名 <T> 定义泛型,可以结合类型断言限制泛型为特定类型
  4. 利用 T extends 类型 的方式约束泛型