TypeScript中的高阶类型函数?

16
请考虑以下伪代码,试图定义一个带有函数类型参数 M<?> 的高阶类型函数:
type HigherOrderTypeFn<T, M<?>> = T extends (...)
  ? M<T>
  : never;

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

我是否正确地认为这样的类型目前在 TS 中是无法表示的?

4个回答

15

你说得对,目前在TypeScript中无法表示。有一个长期的开放GitHub功能请求microsoft/TypeScript#1213,它应该被命名为“支持更高级别的类型”,但目前的标题是“允许类在其他参数化类中具有参数性”。

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

但无论如何,如果您想增加这个问题被解决的机会(不幸的是可能很小),您可能需要去那个问题并给出一个赞和/或描述您的用例,如果您认为它与已经存在的内容相比特别引人注目!


1
感谢@jcalz澄清了这一点!这是我进行的一项实验(试图DRY up我的打字),但我同意在生产中以更简洁(尽管更冗余)的方式实现这一点会更好。不过很高兴看到这样的构造在当前的TS语义下是可行的。 - brncsk
2
这个讨论让我想到未来的编程语言会很像 Haskell... - Lucas Menicucci

8

如果您和其他人正在寻找一种解决方法,可以尝试基于占位符的简单想法(请参见jcalz提到的讨论中的此评论):

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)

4

如果您遇到此问题,可以在TypeScript的讨论群中找到一个不错的示例:

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 并返回一个 FooSet<T>SetFactory 包含多个方法,如 createNumberSet,它返回给定类型的新集合,并将类型参数设置为 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 的信息存储在 FooSet 的静态 hkt 属性中来实现。你需要为每个支持的类执行此操作。
  • 现在我们有了 Hkt.Output<FooSetHkt, number>。解析 Hkt.Output 类型别名,我们得到 (FooSetHkt & { [Hkt.input]: number })[typeof Hkt.output]。唯一的符号 Hkt.input / Hkt.output 有助于创建唯一的属性,但我们也可以使用唯一的字符串常量。
  • 现在我们需要访问 FooSetHktHkt.output 属性。对于每个类,这都是不同的,并包含有关如何使用类型参数构造具体类型的详细信息。FooSetHkt 将输出属性定义为类型 FooSet<Hkt.Input<this>>
  • 最后,Hkt.Input<this> 只是访问 FooSetHktHkt.input 属性。它将解析为 unknown,但通过使用交集 FooSetHkt & { [Hkt.input]: number },我们可以将 Hkt.input 属性更改为 number。因此,如果我们已经达到目标,Hkt.Input<this> 解析为 numberFooSet<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>;

2

fp-ts中,有一种利用模块扩展的HKT实现。

作者在这里记录的解决Higher Kinded Types的方法是:

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;
}

请看这个问题:typescript与fp-ts和URI中的高阶类型

或许会有所启发。

祝好!


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