Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript 泛型

类型别名中的泛型#

type Partial<T> = {
    [P in keyof T]?: T[P];
};

interface IFoo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type PartialIFoo = Partial<IFoo>;

// 等价于
interface PartialIFoo {
  prop1?: string;
  prop2?: number;
  prop3?: boolean;
  prop4?: () => void;
}

类型别名与泛型的结合中,除了映射类型、索引类型等类型工具以外,还有一个非常重要的工具:条件类型

type IsEqual<T> = T extends true ? 1 : 2;

type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2

泛型约束与默认值#

type Factory<T = boolean> = T | number | string;

除了声明默认值以外,泛型还能做到一样函数参数做不到的事:泛型约束。也就是说,你可以要求传入这个工具类型的泛型必须符合某些条件,否则你就拒绝进行后面的逻辑。在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extends,A extends B 意味着 A 是 B 的子类型,这里我们暂时只需要了解非常简单的判断逻辑,也就是说 A 比 B 的类型更精确,或者说更复杂。具体来说,可以分为以下几类。

  • 更精确,如字面量类型是对应原始类型的子类型,即 'linbudu' extends string599 extends number 成立。类似的,联合类型子集均为联合类型的子类型,即 11 | 21 | 2 | 3 | 4 的子类型。
  • 更复杂,如 { name: string }{} 的子类型,因为在 {} 的基础上增加了额外的类型,基类与派生类(父类与子类)同理。
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';


type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"

type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。

在 TypeScript 中,泛型参数存在默认约束(在下面的函数泛型、Class 泛型中也是)。这个默认约束值在 TS 3.9 版本以前是 any,而在 3.9 版本以后则为 unknown。在 TypeScript ESLint 中,你可以使用 no-unnecessary-type-constraint 规则,来避免代码中声明了与默认约束相同的泛型约束。

多泛型关联#

type Conditional<Type, Condition, TruthyResult, FalsyResult> =
  Type extends Condition ? TruthyResult : FalsyResult;

//  "passed!"
type Result1 = Conditional<'linbudu', string, 'passed!', 'rejected!'>;

// "rejected!"
type Result2 = Conditional<'linbudu', boolean, 'passed!', 'rejected!'>;

多泛型参数其实就像接受更多参数的函数,其内部的运行逻辑(类型操作)会更加抽象,表现在参数(泛型参数)需要进行的逻辑运算(类型操作)会更加复杂。
上面我们说,多个泛型参数之间的依赖,其实指的即是在后续泛型参数中,使用前面的泛型参数作为约束或默认值:

type ProcessInput<
  Input,
  SecondInput extends Input = Input,
  ThirdInput extends Input = SecondInput
> = number;

对象类型中的泛型#

interface IRes<TData = unknown> {
  code: number;
  error?: string;
  data: TData;
}
interface IUserProfileRes {
  name: string;
  homepage: string;
  avatar: string;
}

function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}

type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
interface IPaginationRes<TItem = unknown> {
  data: TItem[];
  page: number;
  totalCount: number;
  hasNextPage: boolean;
}

function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {}

函数中的泛型#

假设我们有这么一个函数,它可以接受多个类型的参数并进行对应处理,比如:

  • 对于字符串,返回部分截取;
  • 对于数字,返回它的 n 倍;
  • 对于对象,修改它的属性并返回。
function handle<T>(input: T): T {}
const handle = <T>(input: T): T => {}
const handle = <T extends any>(input: T): T => {}

const author = "linbudu"; // 使用 const 声明,被推导为 "linbudu"

let authorAge = 18; // 使用 let 声明,被推导为 number

handle(author); // 填充为字面量类型 "linbudu"
handle(authorAge); // 填充为基础类型 number

我们为函数声明了一个泛型参数 T,并将参数的类型与返回值类型指向这个泛型参数。这样,在这个函数接收到参数时,T 会自动地被填充为这个参数的类型。这也就意味着你不再需要预先确定参数的可能类型了,而在返回值与参数类型关联的情况下,也可以通过泛型参数来进行运算
在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。

function swap<T, U>([start, end]: [T, U]): [U, T] {
  return [end, start];
}

const swapped1 = swap(["linbudu", 599]);
const swapped2 = swap([null, 599]);
const swapped3 = swap([{ name: "linbudu" }, {}]);

函数的泛型参数也会被内部的逻辑消费,如:

function handle<T>(payload: T): Promise<[T]> {
  return new Promise<[T]>((res, rej) => {
    res([payload]);
  });
}

Class 中的泛型#

class Queue<TElementType> {
  private _list: TElementType[];

  constructor(initial: TElementType[]) {
    this._list = initial;
  }

  // 入队一个队列泛型子类型的元素
  enqueue<TType extends TElementType>(ele: TType): TElementType[] {
    this._list.push(ele);
    return this._list;
  }

  // 入队一个任意类型元素(无需为队列泛型子类型)
  enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
    return [...this._list, element];
  }

  // 出队
  dequeue(): TElementType[] {
    this._list.shift();
    return this._list;
  }
}

内置方法中的泛型#

function p() {
  return new Promise<boolean>((resolve, reject) => {
    resolve(true);
  });
}
const arr: Array<number> = [1, 2, 3];

// 类型“string”的参数不能赋给类型“number”的参数。
arr.push('linbudu');
// 类型“string”的参数不能赋给类型“number”的参数。
arr.includes('linbudu');

// number | undefined
arr.find(() => false);

// 第一种 reduce
arr.reduce((prev, curr, idx, arr) => {
  return prev;
}, 1);

// 第二种 reduce
// 报错:不能将 number 类型的值赋值给 never 类型
arr.reduce((prev, curr, idx, arr) => {
  return [...prev, curr]
}, []);

reduce 方法是相对特殊的一个,它的类型声明存在几种不同的重载:

  • 当你不传入初始值时,泛型参数会从数组的元素类型中进行填充。
  • 当你传入初始值时,如果初始值的类型与数组元素类型一致,则使用数组的元素类型进行填充。即这里第一个 reduce 调用。
  • 当你传入一个数组类型的初始值,比如这里的第二个 reduce 调用,reduce 的泛型参数会默认从这个初始值推导出的类型进行填充,如这里是 never[]

其中第三种情况也就意味着信息不足,无法推导出正确的类型,我们可以手动传入泛型参数来解决:

arr.reduce<number[]>((prev, curr, idx, arr) => {
  return prev;
}, []);

References:
https://juejin.cn/book/7086408430491172901

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。