如何将区分联合技术应用于函数签名?

How to apply discriminating union technique to function signatures?

我想将区分联合技术应用于函数签名。

代码示例

type fun1 = (opts: {type: 'a', value: string}) => string;
type fun2 = (opts: {type: 'b', value: number}) => number;

type anyFun = fun1 | fun2;

class Clazz {
  innerMethod<T extends anyFun>(opts: Parameters<T>[0]): ReturnType<T> {
    return 123 as ReturnType<T>;
  }

  otherMethod() {
    // Case 1 - FAILED
    // expected result type: string
    // actual result type: string | number
    const result1 = this.innerMethod({type: 'a', value: 'str'});

    // Case 2 - OK
    // expected result type = actual type = string
    const result2 = this.innerMethod<fun1>({type: 'a', value: 'str'});

    // Case 3 - OK
    // TS error that fun2 is incompatible with type: 'a'
    const result3 = this.innerMethod<fun2>({type: 'a', value: 'str'});
  }
}

我想以案例 1 的方式编写代码而无需显式类型声明,但 TypeScript 类型推断 returns 在这种情况下类型错误。

如何使用函数签名以这种方式编写代码?或者唯一的方法是更改​​函数签名类型?

更新。更多信息我想解决什么问题。

我有一个包装服务集的外观。

每个服务都有 API 并且其中一部分 API 是同步的 getter。

服务可以通过getter向其他服务请求信息。

Getter 可以描述为函数签名。

我想在两个地方使用这个签名:

  1. 实现 getter 的服务并且运行良好。
  2. 调用 getter 的服务,这里我遇到了区分联合的问题。

代码:

type Fork = number;
type Spoon = string;

type GetFork = (arg: {type: 'fork', id: number}) => Fork;
type GetSpoon = (arg: {type: 'spoon', id: number}) => Spoon;

interface Facade {

  constructor(services: AbstractService[]);

  // routes request to service which can execute getter
  get(request): any;
}

class AbstractService<NeedGetters extends (arg: any) => any = never> {
  facade!: Facade;

  init(facade: Facade) {
    this.facade = facade;
  }

  getFromFacade<T extends NeedGetters>(opts: Parameters<T>[0]): ReturnType<T> {
    return this.facade.get(opts);
  }
}
interface ForksService extends AbstractService {
  getFork({type}: Parameters<GetFork>[0]): ReturnType<GetFork>;
}

interface SpoonsService extends AbstractService {
  getSpoon({type}: Parameters<GetSpoon>[0]): ReturnType<GetSpoon>;
}

class StuffService extends AbstractService<GetFork | GetSpoon> {
  someMethod() {
    // TS infered type string | number
    const fork = this.getFromFacade({type: 'fork', id: 25});

    // TS infered type string | number
    const spoon = this.getFromFacade({type: 'spoon', id: 36});
  }
}

我不确定为什么在似乎没有这种类型的函数时将其表示为函数签名,但是如果我想编写一个允许类型推断的 innerMethod() 版本,我会这样做:

innerMethod<P extends Parameters<AnyFun>[0]>(
  opts: P
): ReturnType<Extract<AnyFun, (opts: P) => any>> {
  return 123 as any;
}

您的版本的问题是编译器无法从 Parameters<T>[0] 类型的值可靠地推断出 T extends AnyFun。编译器不知道如何从中“逆向工程”正确的 T 类型。最可靠的类型推断形式是为编译器提供您要推断的类型的值,然后计算其他类型。

上面的类型P就是传入的opts参数的类型,我们是constraining它到Parameters<AnyFun>[0],也就是

{
    type: "a";
    value: string;
} | {
    type: "b";
    value: number;
}

我们有一个 P,我们可以在 that 上使用 the Extract utility type to get the member of AnyFun whose first parameter is of type P, and then use ReturnType。你可以看到它现在可以工作了:

otherMethod() {
  const result1 = this.innerMethod({ type: 'a', value: 'str' }); // string
  result1.toUpperCase(); // okay
}

但正如我所说,用函数来表示这样的类型映射对我来说似乎很奇怪。在没有关于您的用例的情有可原的信息的情况下,我倾向于像这样重写您的代码:

type IOMapping = { a: string, b: number }

innerMethod<K extends keyof IOMapping>(
  opts: { type: K, value: IOMapping[K] }
): IOMapping[K] {
  return 123 as any;
}

它在类型推断方面的行为相似,但需要的类型操作要少得多。

Playground link to code