TS 泛型通关宝典

1,815 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 5 天,点击查看活动详情

背景

在实际开发中,TS 应用的好不好,泛型的使用就能体现出来,到底是一个 ts 初级还是一个 ts 高阶呢?还有面试官考察 ts,基本也是出泛型相关题目。其重要性不言而喻了。

泛型学习资料其实很多,但是实际应用场景呢,实战中怎么把它用出来,用好呢,又是一个问题,接下来我们就统一整理一下实战场景应用,如有不全,请小伙伴们在留言区补充。

奥,之前有总结 TS 整体的笔记,可查看:TypeScript 入门修炼至出关TS 项目中的使用# 再学Typescript,只为更好的运用它

泛型的定义

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

泛型就像一个占位符一个变量,在使用的时候我们可以将定义好的类型像参数一样传入,原封不动的输出。

设计泛型的关键目的是在成员之间提供有意义的约束, 这些成员可以是:

接口(interface)
别名 (type)
类的实例成员
类的方法
函数参数
函数返回值
泛型自己的约束

实战宝典

接口的约束

interface IInfo<T> {
  nickname: T;
  address: T;
}

const T13: IInfo<string> = {
  nickname: "Nate",
  address: 23423 // err
};

interface IInfoN<T> {
  nickname: T;
  address: string;
}

const KK: IInfoN<number | string> = {
  nickname: 234,
  address: "asdf"
};
KK.nickname = "";

定义函数的形状

interface ICreateArrayFunc {
  <T>(length: number, value: T): Array<T>;
}

let createArray: ICreateArrayFunc;

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

限制函数参数及返回值类型

function test<T>(name: T): T {
  return name;
}

let str: string;
str = test<number>(234); // err

// 泛型的自动推断
str = test(23);  // err

限制构造函数参数类型以及类成员变量、方法参数的类型

class Animal<T> {
  name: T;
  constructor(name: T) {
    this.name = name;
  }
  action(say: T): T {
    return say;
  }
}

let dog = new Animal<string>("旺财");
dog.action("23432");

定义多个类型参数

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap<number, string>([7, "seven"]); // ['seven', 7]

参数的默认类型

当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

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

createArray(23, {});
createArray<number>(23, 23);

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function loggingIdentity<T>(arg: T): T {
  if (arg.length) {
    console.log("arg");
  }
  return arg;
}

loggingIdentity("ssss");

我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface ILengthWise {
  length: number;
}

function loggingIdentity<T extends ILengthWise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([]);

多个类型参数之间也可以互相约束

我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id];
  }
  return target;
}

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

copyFields(x, x2);

泛型接应用

// 改造前
function createArray(length: number, value: any): Array<any> {
  let result = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray(3, "x"); // ['x', 'x', 'x']
// 改造后
function createArray<T>(length: number, value: T): Array<T> {
  let result = [];
  for (let i = 0; i < length; i++) {
      result[i] = value;
  }
  return result;
}

createArray<number>(3, 23); // ['x', 'x', 'x']

Promise 返回值的约束

function readFileAsync(path: string): Promise<string> {
  return new Promise(resolve => {
    resolve("234");
  });
}

function cloneAsync<T>(
  originObject: T,
  callback?: (err: string, res: T) => void
): Promise<T> {
  const data = {};
  return new Promise(resolve => {
    resolve(originObject);
  });
}

cloneAsync<{ code: string; data: object; msg: string }>(234, () => {}).then(
  res => {
    console.log(res.code, res.data, res.msg);
  }
);

// 不加类型
cloneAsync({ code: 12 }, () => {}).then(res => {
  console.log(res.code);
});

接口函数的应用

import Axios, { AxiosRequestConfig } from "axios";

const fetch = <T>(config: AxiosRequestConfig): Promise<T> => {
  return Axios(config).then(res => {
    return res.data;
  });
};

export function POST<T = any>(config: AxiosRequestConfig) {
  config.method = "POST";
  return fetch<T>(config);
}

interface IMatch {
  matchTypes: string[];
}

function getMatchType(nid: number) {
  return POST<HResponseData<{ matchTypes: string[] }>>({
    url: "/common/plugin/match-type",
    data: { id: nid }
  });
}

let matchTypes: number;
const getType = async () => {
  try {
    const { data, code, msg } = await getMatchType(123);
    if (code === 4) {
      const matchTypes = data.matchTypes;
    }
    alert(msg);
  } catch (error) {
    console.log("catch error:", error);
  }
};

keyof

keyof 关键字让我们能够获取一个 interface 的全部的 key

keyof 用于遍历某种类型的属性(可以操作接口、类以及基本数据类型)

interface IStu {
  name: string;
  age: number;
  nest: [];
}

type deepKeys = keyof IStu; // 'name' | 'age' | 'nest'

// let dd: deepKeys = "323"; // err
let dd: deepKeys = "nest";

console.log(dd);

typeof

typeof 操作符用于获取变量的类型。因此这个操作符的后面接的始终是一个变量,且需要运用到类型定义当中

const letter = {
  a: 1,
  b: "ssss",
  c: false
};

type TL = typeof letter;

const conditionData: TL = {
  a: 2,
  b: "here are some words",
  c: true
};

TS 中的泛型工具

IPartial

创建一个符合目标接口其中的部分类型的新类型

type IPartial<T> = { [key in keyof T]?: T[key] };

interface IConditionData {
  platId: string;
  appId: string;
  secretKey: string;
  payType: number;
}

const con: Partial<IConditionData> = {
  platId: "234"
  // appIds: '234',
  // secretKey: '234',
  // payTypes: 123
};

type IConditionDataPartial = IPartial<IConditionData>;

const condition: IConditionDataPartial = {
  platId: "234"
};

IPick<IConditionDatas, kkd>

从接口里选取几个属性类型做为一个新接口

IPick<IConditionDatas, kkd> 使用Pick来在Type中拾取keys对应的属性

interface IConditionData {
  platId: string;
  appId: string;
  secretKey: string;
  payType: number;
}

const con: keyof IConditionData = 'appId'

type IPick<T, K extends keyof T> = {
  [key in K]: T[key]
}

type aps = 'appId' | 'platId' | 'secretKey'
type IConditionDataPick = IPick<IConditionData, aps>


const dataPick: IConditionDataPick = {
  platId: '234',
  appId: '234',
  secretKey: '234'
}

console.log('dataPick: ', dataPick);

Record<keys, Type>

使用 Record 生成新类型,包含 keys 对应的属性键名,键值为 Type,常用来实现一种类型对另外一种类型的映射关系:

type Record<K extends keyof any, T> = { [P in K]: T };

type Page = "home" | "about" | "concat";

interface PageInfo {
  title: string;
}

const nav: Record<Page, PageInfo> = {
  home: { title: "231" },
  about: { title: "231" },
  concat: { title: "231" }
};

Exclude<Type, ExcludedUnion>

在 Type 中移除 ExcludedUnion 中的所有类型(差集)

type Exclude<T, U> = T extends U ? never : T;

interface I1 {
  name3: string;
  age: number;
}

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

type T3 = Exclude<I1, I2>;

const dd: T3 = {
  name3: "324",
  age: 234
};
console.log("dd: ", dd);

interface IConditionData {
  platId: string;
  appId: string;
  secretKey: string;
  payType: number;
}

interface ICond {
  platId: string;
}

const DD: Exclude<ICond, IConditionData> = {
  platId: "2342",
  appId: "2342",
  appId3: "2342"
};

Omit

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

从 Type 中生成所有属性,并且移除 key 对应的属性

删除接口 Type 中 keys 中键组成新的接口

interface ITodo {
  title: string;
  description: string;
  completed: string;
}

type TodoPreview = Omit<ITodo, "description">;

const todo: TodoPreview = {
  title: "hahaha",
  completed: "667"
};

IExtract

生成 Type 与 Union 的交集

第一个泛型参数 必须是继承 第二个的

type Extract<T, U> = T extends U ? T : never;

interface I1 {
  name: string;
  age: number;
  // dta: number
}

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

type T3 = Extract<I1, I2>;

const d: T3 = {
  name: "234",
  age: 234
  // dta: 234
};

new 可实例化

interface ICallMeWithNewToGetString {
  new (): string;
}

// 使用
declare const Foo: ICallMeWithNewToGetString;
const bar = new Foo(); // bar 被推断为 string 类型

本文随时可能更新,关注不迷路!