TypeScript 中的高阶类型函数?

Higher-order type functions in TypeScript?

请考虑以下伪代码,它试图定义一个带有函数类型参数的高阶类型函数 M<?>

type HigherOrderTypeFn<T, M<?>> = T extends (...)
  ? M<T>
  : never;

M<?> 在语法上是不正确的 TypeScript,但将类型签名声明为 HigherOrderTypeFn<T, M> 会在第二行产生错误 Type 'M' is not generic. ts(2315)

我假设这种类型目前在 TS 中无法表示是否正确?

你是对的,它目前无法在 TypeScript 中表示。有一个长期开放的 GitHub 功能请求 microsoft/TypeScript#1213,它的标题可能应该类似于 "support higher kinded types",但目前的标题是 "Allow classes to be parametric in other parametric classes"。

关于如何在当前语言中模拟此类更高种类的类型的讨论中有一些想法(有关具体示例,请参见 this comment),但我认为它们可能不属于生产代码.如果您有一些特定的结构要实现,也许可以提出一些合适的建议。

但在任何情况下,如果您想增加这种情况发生的可能性(不幸的是,这种情况可能微不足道),您可能需要解决该问题并给它一个 and/or 描述您的用例,如果您认为与已有的内容相比,它特别引人注目。好的,希望有所帮助;祝你好运!

对于您和其他正在寻找解决方法的人,您可以尝试一个基于占位符的简单想法(请参阅 jcalz 提到的讨论中的 this comment):

type Placeholder = {'aUniqueKey': unknown};
type Replace<T, X, Y> = {
  [k in keyof T]: T[k] extends X ? Y : T[k];
};

因此,您的函数如下所示:

type HigherOrderTypeFn<T, M> = T extends (...) ? Replace<M, Placeholder, T> : never;

并被称为例如:

type M<U> = U[];
type X = HigherOrderTypeFn<number, M<Placeholder>> // is number[] (if ... is number)

对于遇到这个问题的人,TypeScript discord 服务器上有这个很好的例子:

export interface Hkt<I = unknown, O = unknown> {
  [Hkt.isHkt]: never,
  [Hkt.input]: I,
  [Hkt.output]: O,
}

export declare namespace Hkt {
  const isHkt: unique symbol
  const input: unique symbol
  const output: unique symbol

  type Input<T extends Hkt<any, any>> =
    T[typeof Hkt.input]

  type Output<T extends Hkt<any, any>, I extends Input<T>> =
    (T & { [input]: I })[typeof output]

  interface Compose<O, A extends Hkt<any, O>, B extends Hkt<any, Input<A>>> extends Hkt<Input<B>, O>{
    [output]: Output<A, Output<B, Input<this>>>,
  }

  interface Constant<T, I = unknown> extends Hkt<I, T> {}
}

可以按如下方式使用。下面的代码片段定义了一个 SetFactory,您可以在其中指定创建工厂时所需的集合类型,例如typeof FooSettypeof BarSettypeof FooSetFooSet 的构造函数,类似于更高种类的类型,构造函数类型采用任何 T 和 returns 和 FooSet<T>SetFactory 包含几个方法,例如 createNumberSet,其中 returns 一组新的给定类型,类型参数设置为 number

interface FooSetHkt extends Hkt<unknown, FooSet<any>> {
    [Hkt.output]: FooSet<Hkt.Input<this>>
}
class FooSet<T> extends Set<T> {
    foo() {} 
    static hkt: FooSetHkt;
}

interface BarSetHkt extends Hkt<unknown, BarSet<any>> {
    [Hkt.output]: BarSet<Hkt.Input<this>>;
}
class BarSet<T> extends Set<T> { 
    bar() {} 
    static hkt: BarSetHkt;
}

class SetFactory<Cons extends {
    new <T>(): Hkt.Output<Cons["hkt"], T>;
    hkt: Hkt<unknown, Set<any>>;
}> {
    constructor(private Ctr: Cons) {}
    createNumberSet() { return new this.Ctr<number>(); }
    createStringSet() { return new this.Ctr<string>(); }
}

// SetFactory<typeof FooSet>
const fooFactory = new SetFactory(FooSet);
// SetFactory<typeof BarSet>
const barFactory = new SetFactory(BarSet);

// FooSet<number>
fooFactory.createNumberSet();
// FooSet<string>
fooFactory.createStringSet();

// BarSet<number>
barFactory.createNumberSet();
// BarSet<string>
barFactory.createStringSet();

其工作原理的简短说明(以 FooSetnumber 为例):

  • 要理解的主要类型是Hkt.Output<Const["hkt"], T>。使用我们的示例类型替换后,这将变为 Hkt.Output<(typeof FooSet)["hkt"], number>。魔术现在涉及将其变成 FooSet<number>
  • 首先我们将(typeof FooSet)["hkt"]解析为FooSetHkt。很多魔法都在这里,通过在 FooSet 的静态 hkt 属性 中存储有关如何创建 FooSet 的信息。您需要为每个支持的 class.
  • 执行此操作
  • 现在我们有 Hkt.Output<FooSetHkt, number>。解析 Hkt.Output 类型别名,我们得到 (FooSetHkt & { [Hkt.input]: number })[typeof Hkt.output]。唯一符号 Hkt.input / Hkt.output 有助于创建唯一属性,但我们也可以使用唯一字符串常量。
  • 现在我们需要访问 FooSetHktHkt.output 属性。这对于每个 class 都是不同的,并且包含有关如何使用类型参数构造具体类型的详细信息。 FooSetHkt 将输出 属性 定义为 FooSet<Hkt.Input<this>>.
  • 类型
  • 最后,Hkt.Input<this> 只是访问了 FooSetHktHkt.input 属性。它会解析为 unknown,但通过使用交集 FooSetHkt & { [Hkt.input]: number },我们可以将 Hkt.input 属性 更改为 number。因此,如果我们达到了目标,Hkt.Input<this> 将解析为 number,而 FooSet<Hkt.Input<this>> 将解析为 FooSet<number>

对于问题中的示例,Hkt.Output 本质上是所要求的,只是类型参数颠倒了:

interface List<T> {}
interface ListHkt extends Hkt<unknown, List<any>> {
    [Hkt.output]: List<Hkt.Input<this>>
}
type HigherOrderTypeFn<T, M extends Hkt> = Hkt.Output<M, T>;
// Gives you List<number>
type X = HigherOrderTypeFn<number, ListHkt>;

fp-ts 中有一个 HKT 的实现(利用模块增强)。

Higher Kinded 类型 as documented here by its author 的解决方法是:

export interface HKT<URI, A> {
  readonly _URI: URI;
  readonly _A: A;
}

并且可以这样使用:

export interface Foldable<F> {
  readonly URI: F;
  reduce: <A, B>(fa: HKT<F, A>, b: B, f: (b: B, a: A) => B) => B;
}

看看这个问题:

也许这能说明一些问题?

干杯