为什么我无法从实现接口的 class 推断类型参数?

Why am i unable to infer type parameters from a class that implements an interface?

背景

我有一个声明单个方法的接口,其 return 类型取决于提供给接口的类型参数。

interface Command<T, U = void> {
    execute(input: T): [U] extends [void] ? T | Observable<T>: T | U | Observable<T|U>;
}

然后我有一个实用程序类型,它应该 return 类型参数 TU 用于提供的类型。

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, A] :
    never;

问题

Playground

虽然 GetCommandParamTypes 在提供 Command 接口时按预期工作,但我正在努力调试为什么我在提供 [=111= 时没有得到预期的结果] 实现该接口。

 type type1 = GetCommandParamTypes<Command<string>>;
// [string, void]

type type2 = GetCommandParamTypes<Command<string, string>>;
// [string, string]

type type3 = GetCommandParamTypes<Command<string | number, string>>;
// [string | number, string]

type type4 = GetCommandParamTypes<Command<string | number, Function>>;
// [string | number, Function]

type type5 = GetCommandParamTypes<Command<string, number | Function>>;
// [string, number | Function]

type type6 = GetCommandParamTypes<TypeACommand>; // expected [string, void]
// [string, string]

type type7 = GetCommandParamTypes<TypeBCommand>; // expected [string, number | Function]
// [string | number | Function, string | number | Function]

type type8 = GetCommandParamTypes<TypeCCommand>; // expected [string, void]
// [string, string | Observable<string>]

type type9 = GetCommandParamTypes<MixedTypeCommand>; // expected [string | number, boolean | Function]
// [string | number | boolean | Function, string | number | boolean | Function]

type type10 = GetCommandParamTypes<MixedTypeObservableCommand>; // expected [string | number, boolean | Function]
// [string | number | boolean | Function, string | number | boolean | Function | Observable<string | number | boolean | Function>]

问题

是否可以从实现接口的 Class 推断类型参数,或者我的实现有问题?

有人可以解释当检查 Class 以推断类型参数时会发生什么,或者向我指出可能有助于解释它的任何资源的方向吗?

更新 1

Playground

如果我从命令 return 类型中删除条件类型(这不是所需的行为,因为第二个类型参数是可选的,我不希望 void 在 return 如果未提供,请键入)它似乎更接近我的预期。或者至少是更可行的东西。

interface Command<T, U = void> {
    execute(input: T): T | U | Observable<T | U>;
}

乍一看似乎 execute 方法 input 类型正在 returned 用于 infer IO 而 return 类型正在 return编辑 infer A.

但对于具有以下签名的 TypeACommand

execute(input: string): string | Observable<string>

[string, string] 是 returned,而不是 [string, string | Observable<string>]

其中 TypeCCommand 具有以下签名:

execute(input: string): Observable<string>

[string, string | Observable<string>] 实际上是 returned 而不是 [string, Observable<string>]

MixedTypeObservableCommand 也只有 Observable 作为 return 类型也 returns Observable<...>Observable本身。

所以:

execute(input: string | number): Observable<string | number | Function | boolean>

变为:

[string | number, string | number | boolean | Function | Observable<string | number | boolean | Function>]

更新 2

这是我之前创建的一个工作示例的 link,在将 Observable 添加到 Command 接口的 return 类型之前。

Playground

至少在 returned 的 infer IOinfer A 版本中似乎是一致的,其中 IOexecute方法输入类型和 Aexecute 方法 return 类型。

然后我可以使用 Exclude 来计算与 Command 接口相关的相应类型

之所以会发生这种情况,是因为当检查 GetCommandParamTypes 中的 TCommand 的一个实例时,它实际上并没有检查您提供的 class实现接口。 Typescript 具有结构性而非名义类型,因此它所做的是比较 Command<..., ...>T 的签名。类型 TypeACommand 有签名

type TypeOfACommandObject = {
    execute(input: string): string | Observable<string>
}

没有任何迹象表明它实现了 Command,它只是一个对象,它有一个带有特定参数和 return 类型的方法 execute。如果现在将此对象传递给 GetCommandParamTypes,您会发现完全相同的问题:您可能希望 return 类型为 [string, void],但实际上是 [string, string]。而且也不矛盾,因为Command<string, string>有签名

{
    execute(input: string): string | string | Observable<string | string>
}

这与 Command<string, void> 的签名没有区别。所以你不能那么容易地输入它。

当您尝试 GetCommandParamType<Command<string, void>> 时它会正常工作的原因,即使 Command<string, void> 的签名如上所述并且您可能希望它也被错误地推断出来,是因为typescript 的编译器实现。我假设当检查类型 T 是否是 Command 的实例时,它首先检查 T 是否是 Command。在这种情况下,它 字面意思和 Command 的实例,因此它只是跳过比较结构并产生正确的类型参数。然而 TypeOfACommandObjectTypeACommand 不是 Command 的直接实例,即使后者实现了它,所以这个快捷方式失败了。不过这是一个猜测,我不熟悉打字稿的实现。


说到修复它,如果不重构 execute 的 return 类型,我真的想不出什么好方法,这样 TU 就不会了合并。我可以想到品牌类型,例如使用 unique symbol 类型向 return 类型的命令添加特殊的唯一签名,但是在实现 Command 和在外部使用它时都很难同时使用.您可能应该尝试以某种方式将 TU 分开


更新:

在更新的示例中,由于 GetCommandTypeParams

的不同定义,它可以工作
type ReplaceNever<T, U> = [T] extends [never] ? U : T;

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, ReplaceNever<Exclude<A, IO>, void>] :
    never;

如果你在第一个例子中坚持同样的定义,你会发现它也几乎按预期工作,唯一的问题是最后的 class 和 Observables。所以你需要稍微调整一下

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, ReplaceNever<A extends Observable<infer U> ? Exclude<U, IO> : Exclude<A, IO>, void>] :
    never;

Sandbox

这个解决方案有一些奇怪的地方,比如你不能完全从实现 Command<string | number, string | Function> 的 classes 中提取类型,但是如果你能接受它就没问题