将特定的函数签名保留在泛型类型约束中 - TypeScript

问题描述

首先:抱歉这篇长文章,事实上我在这里有两个不同的问题。但由于两者之间的关系如此密切(实现相同目标的两种不同方法),我认为最好将它们放在一个帖子中。

我正在尝试实现一个轻量级的 TypeScript 信号/事件框架,其中

  • 已知所有可能的信号名称支持智能感知
  • 它们的签名(参数和返回值)是强类型的,并且也支持智能感知

我知道存在一些以某种方式实现类似目标的解决方案,但我有一个特定的 简单易用的设计利用了较新的 TypeScript 通用索引类型约束功能 - 但现在我显然无法自己掌握它们:)

我使用的是 TypeScript 4.2.3。

我尝试了两种不同的方法,但结果不同......

第一种方法

这可以编译,但我们在信号参数和返回值上没有任何类型:

// Define signals by name and a function,which specifies SignalArgs
// and SignalValue (return value).
type Signals = {
  hello: (name: string) => string;
  add: (val1: number,val2: number) => number;
};

// Generic class which should implement on() and emit() with arguments
// bound to the specific signals enumerated in the Signals type.
class SignalBus<Signals> {
  on<Key extends string & keyof Signals,Spec extends (...args: any) => any & Signals[Key]>(
    signal: Key,handler: (
      signal: Signal<Parameters<Spec>,ReturnType<Spec>>,...args: Parameters<Spec>
    ) => Promise<ReturnType<Spec>>,): SignalConnection<Parameters<Spec>,ReturnType<Spec>> {
    // ...
    console.log("on",signal,handler);
    const connection = (handler as unkNown) as SignalConnection<Parameters<Spec>,ReturnType<Spec>>;
    connection.signal = signal;
    return connection;
  }

  async emit<Key extends string & keyof Signals,...args: Parameters<Spec>
  ): Promise<Signal<Parameters<Spec>,ReturnType<Spec>>> {
    // ...
    console.log("emit",args);
    return new Signal<Parameters<Spec>,ReturnType<Spec>>(signal,args);
  }
}

// Signal objects represent a signal at runtime
class Signal<SignalArgs extends unkNown[] = [],SignalValue = any> {
  public value: SignalValue;
  constructor(public signal: string,public args: SignalArgs) {}
}

// SignalConnection objects enclose the handler callback,signal name and probably more
type SignalConnection<SignalArgs extends unkNown[],SignalResult> = {
  (signal: Signal<SignalArgs,SignalResult>,...args: SignalArgs): Promise<SignalResult | void>;
  signal: string;
};

// Create a SignalBus specific to the Signals defined above
const bus = new SignalBus<Signals>();

// Fine
bus.on("hello",async (_signal,name) => `Hello ${name}`);

// Should give type error,because two numeric arguments and numeric return value are expected
bus.on("add",name: string) => `Shouldn't be accepted ${name}`);

// Should give type error,because only one string argument is expected
bus.emit("hello","World","shouldn't be accepted");

问题在于特定的函数签名,例如在 on() 方法中,迷路了:

on<Key extends string & keyof Signals,Spec extends (...args: any) => any & Signals[Key]>(...)

使用 Spec extends (...args: any) => any 会发生这种情况。

我知道我必须将 Signals[Key] 属性限制为可调用函数使 Parameters<>ReturnValue<> 实用程序类型起作用。

但是如何在不丢失 Signals[Key] 函数的类型信息的情况下做到这一点?

有什么方法可以实现我在这里要做的事情吗?

另一种方法,另一个问题;)

这里我定义了一个通用的 signal<> 类型,它由信号参数和返回值类型组成,并通过索引访问它们 - 但也没有运气......

当使用 SignalBus 类时,编译器会抱怨不可分配的 never 类型。查看代码示例底部的注释:

// Generic type to specify signal arguments and return value.
type signal<SignalArgs extends unkNown[] = [],SignalResult = void> = {
  args: SignalArgs;
  result: SignalResult;
};

// Define signals by name and the generic signal<> type.
type Signals = {
  hello: signal<[name: string],string>;
  add: signal<[val: number,val2: number],string>;
};

// Generic class which should implement on() and emit() with arguments
// bound to the specific signals enumerated in the Signals type.
class SignalBus<Signals> {
  on<Key extends string & keyof Signals,Spec extends signal & Signals[Key]>(
    signal: Key,handler: (
      signal: Signal<Spec["args"],Spec["result"]>,...args: Spec["args"]
    ) => Promise<Spec["result"]>,): SignalConnection<Spec["args"],Spec["result"]> {
    // ...
  }

  async emit<Key extends string & keyof Signals,...args: Spec["args"]
  ): Promise<Signal<Spec["args"],Spec["result"]>> {
    // ...
  }
}

// Signal objects represent a signal at runtime
class Signal<SignalArgs extends unkNown[] = [],...args: SignalArgs): Promise<SignalResult | void>;
  signal: string;
};

// Create a SignalBus specific to the Signals defined above
const bus = new SignalBus<Signals>();

// Type 'Promise<string>' is not assignable to type 'Promise<never>'.
//   Type 'string' is not assignable to type 'never'. ts(2322)
bus.on("hello",name) => `Hello ${name}`);

// Argument of type '(_signal: Signal<never,never>,name: string) => Promise<string>' is not assignable to parameter of type '(signal: Signal<never,...args: never) =>
// Promise<never>'.
//   Type 'Promise<string>' is not assignable to type 'Promise<never>'. ts(2345)
bus.on("add",name: string) => `Shouldn't be accepted ${name}`);

// Argument of type 'string' is not assignable to parameter of type 'never'. ts(2345)
bus.emit("hello","shouldn't be accepted");

看起来类型约束 Spec extends signal & Signals[Key] 从来没有 匹配。为什么?

有人对此有任何想法吗?

解决方法

由于泛型之一中的以下扩展子句,该方法不起作用:

Spec extends (...args: any) => any & Signals[Key]

由于示例中的 Signals 应该始终是键/函数对的集合,因此它最终将被评估为如下所示:

Spec extends (...args: any) => any & ((exampleArg: string) => string)

(...args: any) => any & ((exampleArg: string) => string) 的交集将始终为 (...args: any) => any。这对于任何其他可能的函数都是正确的。需要的是将 Signals 限制为始终是键/函数对的集合。因此,使此示例工作所需的更改如下:

// add restriction to Signals
class SignalBus<Signals extends Record<string,(...args: any[]) => ()>> {
                               // remove (...args: any[]) => any &
  on<Key extends string & keyof Signals,Spec extends Signals[Key]>(
    signal: Key,handler: (
      signal: Signal<Parameters<Spec>,ReturnType<Spec>>,...args: Parameters<Spec>
    ) => Promise<ReturnType<Spec>>,): SignalConnection<Parameters<Spec>,ReturnType<Spec>> {
    // ...
    console.log("on",signal,handler);
    const connection = (handler as unknown) as SignalConnection<Parameters<Spec>,ReturnType<Spec>>;
    connection.signal = signal;
    return connection;
  }
                                         // remove (...args: any) => any &
  async emit<Key extends string & keyof Signals,...args: Parameters<Spec>
  ): Promise<Signal<Parameters<Spec>,ReturnType<Spec>>> {
    // ...
    console.log("emit",args);
    return new Signal<Parameters<Spec>,ReturnType<Spec>>(signal,args);
  }
}

playground