返回介绍

函数中的泛型

发布于 2024-09-11 00:55:48 字数 3620 浏览 0 评论 0 收藏 0

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

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

这个时候要怎么对函数进行类型声明?是 any 大法好?

function handle(input: any): any {}

还是用联合类型来包括所有可能类型?

function handle(input: string | number | {}): string | number | {} {}

第一种肯定要直接 pass,第二种虽然麻烦了一点,但似乎可以满足需要?

但如果真的调用一下就知道不合适了。

const shouldBeString = handle('wangxiaobai');
const shouldBeNumber = handle(18);
const shouldBeObject = handle({ name: 'wangxiaobai' });

虽然约束了入参的类型,但返回值的类型并没有像预期的那样和入参关联起来,上面三个调用结果的类型仍然是一个宽泛的联合类型 string | number | {}。

难道要用重载一个个声明可能的关联关系?

function handle(input: string): string
function handle(input: number): number
function handle(input: {}): {}
function handle(input: string | number | {}): string | number | {} { }

如果再多一些复杂的情况,别说你愿不愿意补充每一种关联了,同事看到这样的代码都会质疑你的水平。

这个时候就该请出泛型了:

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

为函数声明一个泛型参数 T,并将参数的类型与返回值类型指向这个泛型参数。这样,在这个函数接收到参数时,T 会自动地被填充为这个参数的类型。

这也就意味着你不再需要预先确定参数的可能类型了,而在返回值与参数类型关联的情况下,也可以通过泛型参数来进行运算。

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

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

const author = 'wangxiaobai'; // 使用 const 声明,被推导为 'wangxiaobai'

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

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

你也可以将鼠标悬浮在表达式上,来查看填充的泛型信息。

再看一个例子:

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

const swapped1 = swap(['wangxiaobai', 18]);
const swapped2 = swap([null, 18]);
const swapped3 = swap([{ name: 'wangxiaobai' }, {}]);

在这里返回值类型对泛型参数进行了一些操作,而同样可以看到其调用信息符合预期。

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

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

而 swap 函数,现在只想处理数字元组的情况:

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

而多泛型关联也是如此,比如 lodash 的 pick 函数,这个函数首先接受一个对象,然后接受一个对象属性名组成的数组,并从这个对象中截取选择的属性部分:

const object = { 'a': 1, 'b': '2', 'c': 3 };

_.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }

这个函数很明显需要在泛型层面声明关联,即数组中的元素只能来自于对象的属性名(组成的字面量联合类型!),因此可以这么写(部分简化):

pick<T extends object, U extends keyof T>(object: T, ...props: Array<U>): Pick<T, U>;

这里 T 声明约束为对象类型,而 U 声明约束为 keyof T。同时对应的其返回值类型中使用了 Pick<t, u=""> 这一工具类型,它与 pick 函数的作用一致,对一个对象结构进行裁剪。 </t,>

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

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

箭头函数的泛型,其书写方式是这样的:

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

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

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文