TypeScript如何为通用函数创建通用类型别名?

27

给定一个有类型的泛型函数,我想为该函数创建一个泛型别名,但似乎我无法这样做。换句话说,以下内容不起作用:

// foo.tsx
function foo<T>(arg: T): T {
  return arg
}

type FooT = typeof foo  // works, but alias isn't generic: <T>(arg: T) => T
type FooParametersT = Parameters<typeof foo>  // sure: [unknown]
type FooReturnT = ReturnType<typeof foo>  // no problem: unknown

type GFooT<T,> = typeof foo<T,>  // yikes
type GFooParametersT<T,> = Parameters<typeof foo<T,>>  // nope
type GFooReturnT<T,> = ReturnType<typeof foo<T,>>  // fails

我真正想做的是获取别人库中函数的类型,然后在其周围构建一个接口。例如:

import { useState } from "react"

type UseStateFnT<T,> = typeof useState<T>
const wrapUseState: (toWrap: UseStateFnT<T,>) => UseStateFnT<T,> = …

我不需要自己重新创建复杂类型的函数签名,这是否可行?

1个回答

33

TS4.7+更新

大家好! TypeScript 4.7将引入microsoft/TypeScript#47607中实现的实例化表达式,这将允许您直接指定泛型函数的类型参数,而无需实际调用该函数,因此不再需要像答案中所示那样滥用类。以下是示例:

type GFooT<T,> = typeof foo<T>  // (arg: T) => T
type GFooParametersT<T,> = Parameters<typeof foo<T>>  // [arg: T]
type GFooReturnT<T,> = ReturnType<typeof foo<T>>  // T

这几乎与问题中的语法相同,只是在类型参数后不支持尾随逗号(但在类型参数声明后支持)。太好了!代码游乐场链接
在TypeScript中有两种不同类型的泛型:泛型函数和泛型类型...看起来你想让编译器为你将一个转换成另一个,这是不直接支持的。请参考上一个TS4.6的答案。

明确一点:

通用的类型具有需要在使用特定类型之前指定的类型参数。例如:

type GenType<T> = (x: T) => T[];
declare const oops: GenType; // error
declare const genT: GenType<string>; // okay
const strArr = genT("hello"); // string[];
const numArr = genT(123); // error!

在这里,{{GenType}}是一种通用类型。您需要指定类型参数以将其用作值的类型,然后生成的类型不再是通用的。{{genT}}函数接受一个字符串并返回一个字符串数组。它不能用作接受数字并返回数字数组的函数。

另一方面,通用{{函数}}具有特定类型,可以像任何可能的类型参数替换一样起作用。当您使用它时,通用函数类型的值仍然是通用的。类型参数附加到调用签名:

type GenFunc = <T>(x: T) => T[];
declare const genF: GenFunc;
const strArr = genF("hello"); // strArr: string[];
const numArr = genF(123); // numArr: number[];

在这里,GenFunc是指通用函数的特定类型。当调用genF函数时,它仍然是通用的。

通用函数(包括通用构造函数)可以被视为通用值,而不是通用类型。


这两种泛型类型是相关的,但TypeScript类型系统不足以描述它们之间的关系。在其他一些语言中,您可能能够定义一个泛型类型来涵盖另一个类型,例如:
type GenFunc = forall T, GenType<T>; // not TS, error

或者

type GenType<T> = instantiate GenFunc with T; // not TS, error

但在TypeScript中是不可能的。也许如果我们得到microsoft/TypeScript#1213中请求的更高级类型,但现在还没有。因此,在类型系统中不能直接将GenFunc转换为GenType

有一些邪恶可怕的方法可以强制编译器根据GenFunc计算GenType。我知道的方法利用了泛型类属性初始化和在TypeScript 3.4中引入的一些用于泛型函数的高阶类型推断。我让编译器认为我正在计算值,而实际上我没有任何值,然后获取其中一个假值的类型:

class GenTypeMaker<T> {
    getGenType!: <A extends any[], R>(cb: (...a: A) => R) => () => (...a: A) => R;
    genType = this.getGenType(null! as GenFunc)<T>()
}
type GenType2<T> = GenTypeMaker<T>['genType']
// type GenType2<T> = (x: T) => T[]

你可以验证GenType2<T>GenType<T>是相同的类型,如果你将GenFunc更改为任何具有一个类型参数的泛型函数,则GenType2<T>也会相应更改。
但我不知道我是否想要推荐任何人实际使用这种方法。它并不能真正地扩展或组合;如果你有一个对象,其中包含一个类型参数的通用函数,并且你想将其转换为一个对象,其中包含指定类型参数的具体函数,那么没有办法在不为每个对象属性执行一次的情况下从类型系统中获取它。

代码的游乐场链接


5
谢谢你的回答,我需要读三遍才开始理解,但这反映了我的 Typescript 细节方面的无知,而不是你的回答有问题。感谢你的认真和清晰阐述!我接受了你的回答,希望未来的评论或回复可以提到类似 "这在 TypeScript <X>.<Y> 版本中已经成为可能..." 这样的内容,因为这种情况在 Stack Overflow 上的很多 TypeScript 问题中都很常见。 - posita
有没有可能使该类仅存在于类型级别?使用“declare”似乎不起作用。我发现了一个案例,可以在过程中使用它,但如果必须在运行时创建类,则不值得。 - geoffrey
@posita 嘿,猜猜看! - jcalz
@geoffrey 我认为我编辑的 TS4.7+ 解决方案是你最好的选择;我认为原始答案在没有发出至少一些运行时代码的情况下是无法工作的。 - jcalz
这是个好消息!我想现在我会选择原始答案+一个 Babel 插件来清理发出的 JS,因为当 TS4.7 发布时采用新语法不会造成破坏性变化。 - geoffrey
哇塞!太好了!(之前没回复你实在抱歉,我出了些问题无法评论。) - posita

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接