typescript:泛型

一、类型别名中的泛型

(1)类型别名如果声明了泛型坑位,那其实就等价于一个接受参数的函数。

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

(2)类型别名中的泛型大多是用来进行工具类型封装,比如映射类型中的工具类型。

type Stringify<T> = {
  [K in keyof T]: string;
};

type Clone<T> = {
  [K in keyof T]: T[K];
};

Stringify 会将一个对象类型的所有属性类型置为 string ,而 Clone 则会进行类型的完全复制。 

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

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

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

在条件类型参与的情况下,通常泛型会被作为条件类型中的判断条件(T extends Condition,或者 Type extends T)以及返回值(即 : 两端的值),这也是我们筛选类型需要依赖的能力之一。

二、泛型约束与默认值

(1) 泛型默认值

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

 (2)泛型约束

1、可以要求传入这个工具类型的泛型必须符合某些条件,否则你就拒绝进行后面的逻辑。

2、在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extends,A extends B 意味着 A 是 B 的子类型。

  • 更精确,如字面量类型是对应原始类型的子类型,即 'linbudu' extends string599 extends number 成立。类似的,联合类型子集均为联合类型的子类型,即 1、 1 | 2 是 1 | 2 | 3 | 4 的子类型。
  • 更复杂,如 { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型,基类与派生类(父类与子类)同理。

(3)泛型约束例子

type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';

这个例子会根据传入的请求码判断请求是否成功,这意味着它只能处理数字字面量类型的参数,因此这里我们通过 extends number 来标明其类型约束,如果传入一个不合法的值,就会出现类型错误:

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”。

与此同时,如果我们想让这个类型别名可以无需显式传入泛型参数也能调用,并且默认情况下是成功地,这样就可以为这个泛型参数声明一个默认值:

type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';

type Res4 = ResStatus; // "success"

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

三、多泛型关联

(1) 不仅可以同时传入多个泛型参数,还可以让这几个泛型参数之间也存在联系。

(2)条件类型下的多泛型参数

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!'>;

这个例子表明,多泛型参数其实就像接受更多参数的函数,其内部的运行逻辑(类型操作)会更加抽象,表现在参数(泛型参数)需要进行的逻辑运算(类型操作)会更加复杂。

(3) 多个泛型参数之间的依赖,其实指的即是在后续泛型参数中,使用前面的泛型参数作为约束或默认值。

type ProcessInput<
  Input,
  SecondInput extends Input = Input,
  ThirdInput extends Input = SecondInput
> = number;
  • 这个工具类型接受 1-3 个泛型参数。
  • 第二、三个泛型参数的类型需要是首个泛型参数的子类型
  • 当只传入一个泛型参数时,其第二个泛型参数会被赋值为此参数,而第三个则会赋值为第二个泛型参数,相当于均使用了这唯一传入的泛型参数
  • 当传入两个泛型参数时,第三个泛型参数会默认赋值为第二个泛型参数的值

 四、对象类型中的泛型

(1) 响应类型结构的泛型处理

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

(2) 这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的泛型坑位,然后在你的请求函数中就可以传入特定的响应类型了

interface IUserProfileRes {
  name: string;
  homepage: string;
  avatar: string;
}

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

type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}

(3) 而泛型嵌套的场景也非常常用,比如对存在分页结构的数据,我们也可以将其分页的响应结构抽离出来

interface IPaginationRes<TItem = unknown> {
  data: TItem[];
  page: number;
  totalCount: number;
  hasNextPage: boolean;
}

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

(4) 这些结构看起来很复杂,但其实就是简单的泛型参数填充而已。就像我们会封装请求库、请求响应拦截器一样,对请求中的参数、响应中的数据的类型的封装其实也不应该落下。甚至在理想情况下,这些结构体封装应该在请求库封装一层中就被处理掉。

 五、函数中的泛型

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

  • 对于字符串,返回部分截取;
  • 对于数字,返回它的 n 倍;
  • 对于对象,修改它的属性并返回。

(2)使用泛型

function handle<T>(input: T): T {}

1、我们为函数声明了一个泛型参数 T,并将参数的类型与返回值类型指向这个泛型参数。这样,在这个函数接收到参数时,T 会自动地被填充为这个参数的类型。这也就意味着你不再需要预先确定参数的可能类型了,而在返回值与参数类型关联的情况下,也可以通过泛型参数来进行运算

2、 在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。

function handle<T>(input: T): T {}

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

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

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

(3)另一个例子

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" }, {}]);

 (4)函数中的泛型同样存在约束与默认值,比如上面的 handle 函数,现在我们希望做一些代码拆分,不再处理对象类型的情况了。

function handle<T extends string | number>(input: T): T {}

(5) swap 函数,现在我们只想处理数字元组的情况。

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

 (6)函数的泛型参数也会被内部的逻辑消费。

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

(7) 对于箭头函数的泛型,其书写方式是这样的。

const handle = <T>(input: T): T => {}

需要注意的是在 tsx 文件中泛型的尖括号可能会造成报错,编译器无法识别这是一个组件还是一个泛型,此时你可以让它长得更像泛型一些:

const handle = <T extends any>(input: T): T => {}

(8)函数的泛型是日常使用较多的一部分,更明显地体现了泛型在调用时被填充这一特性,而类型别名中,我们更多是手动传入泛型。这一差异的缘由其实就是它们的场景不同,我们通常使用类型别名来对已经确定的类型结构进行类型操作,比如将一组确定的类型放置在一起。而在函数这种场景中,我们并不能确定泛型在实际运行时会被什么样的类型填充。

(9)需要注意的是,不要为了用泛型而用泛型。(泛型参数 T 没有被返回值消费,也没有被内部的逻辑消费的情况

六、Class中的泛型

(1) Class 中的泛型和函数中的泛型非常类似,只不过函数中泛型参数的消费方是参数和返回值类型,Class 中的泛型消费方则是属性、方法、乃至装饰器等。同时 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;
  }
}

其中,enqueue 方法的入参类型 TType 被约束为队列类型的子类型,而 enqueueWithUnknownType 方法中的 TType 类型参数则不会受此约束,它会在其被调用时再对应地填充,同时也会在返回值类型中被使用。

七、内置方法中的泛型

(1) TypeScript 中为非常多的内置对象都预留了泛型坑位,如 Promise 中。

function p() {
  return new Promise<boolean>((resolve, reject) => {
    resolve(true);
  });
}

在你填充 Promise 的泛型以后,其内部的 resolve 方法也自动填充了泛型,而在 TypeScript 内部的 Promise 类型声明中同样是通过泛型实现:

interface PromiseConstructor {
    resolve<T>(value: T | PromiseLike<T>): Promise<T>;
}

declare var Promise: PromiseConstructor;

(2) 还有数组 Array<T> 当中,其泛型参数代表数组的元素类型,几乎贯穿所有的数组方法:

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;
}, []);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
泛在TypeScript中是一种特性,它允许我们在定义函数、接口或类时不预先指定具体的类型,而是在使用时手动指定类型。它可以帮助我们在编写可重用的代码时增加类型的安全性和灵活性。 在TypeScript中,使用尖括号(< >)来表示泛型,并在类名、函数名或接口名后面使用泛型变量来表示类型参数。例如,我们可以定义一个泛型函数来实现数组元素的反转: ```typescript function reverse<T>(array: T[]): T[] { return array.reverse(); } const numbers = [1, 2, 3, 4, 5]; const reversedNumbers = reverse(numbers); // [5, 4, 3, 2, 1] const names = ["Alice", "Bob", "Charlie"]; const reversedNames = reverse(names); // ["Charlie", "Bob", "Alice"] ``` 在上面的例子中,泛型变量`T`表示类型参数,它可以在函数体内部和函数的参数类型中使用。 除了函数,我们还可以使用泛型来定义类和接口。例如,我们可以创建一个泛型类`Box`来存储不同类型的值: ```typescript class Box<T> { private value: T; constructor(value: T) { this.value = value; } getValue(): T { return this.value; } } const numberBox = new Box<number>(42); console.log(numberBox.getValue()); // 42 const stringBox = new Box<string>("Hello"); console.log(stringBox.getValue()); // "Hello" ``` 上面的例子中,泛型变量`T`可以在类的成员变量类型、构造函数参数类型和方法返回值类型中使用。 总结来说,TypeScript泛型提供了一种灵活的方式来增加代码的可重用性和类型安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值