通过接口实现推断 TypeScript 泛型

8

我试图从传入的泛型参数推断出方法的返回类型。但是,该参数是来自于一个泛型接口的实现,因此我认为typescript推断应该可以从该参数的基类中确定类型。

示例代码:

interface ICommand<T> {}

class GetSomethingByIdCommand implements ICommand<string> {
  constructor(public readonly id: string) {}
}

class CommandBus implements ICommandBus {
  execute<T>(command: ICommand<T>): T {
    return null as any // ignore this, this will be called through an interface eitherway
  }
}

const bus = new CommandBus()
// badResult is {}
let badResult = bus.execute(new GetSomethingByIdCommand('1'))

// goodResult is string
let goodResult = bus.execute<string>(new GetSomethingByIdCommand('1'))

我希望做的是第一个 execute 调用,并让 TypeScript 推断出正确的返回值,在这种情况下,根据实现自 GetSomethingByIdCommand 推导得到的是 string
我尝试过使用 条件类型 进行操作,但不确定是否可行或如何应用。

1
也许问题在于你的 ICommand<T> 接口没有对 T 的使用进行限制。因此,你的 GetSomethingByIdCommand隐式实现ICommand<number>(例如)。实际上,该类似乎是 ICommand<T> 的实现 *适用于任何类型 T*。鉴于此,TypeScript 如何选择推断哪种类型呢? - CRice
1
@CRice,我不太理解这个。当GetSomethingByIdCommand显式实现ICommand<string>时,它是如何隐式实现ICommand<number>的?能否详细解释一下? - Jake Holzinger
3个回答

10
您的问题在于 ICommand<T> 不是结构上依赖于 T(如 @CRice 的评论中所提到的)。
这是不推荐的。(⬅ 链接到一个 TypeScript FAQ 条目,详细介绍了一个与此案例几乎完全相同的情况,因此这是我们可能在此处获得的最接近官方说法)
TypeScript 的类型系统(大多数情况下)是结构性的,而不是名义上的:如果两个类型具有相同的形状(例如具有相同的属性),则它们是相同的类型,这与它们是否具有相同的名称无关。如果 ICommand<T> 在结构上不依赖于 T,并且它的任何属性都与 T 无关,则 ICommand<string> 是与 ICommand<number> 相同的类型,与 ICommand<ICommand<boolean>> 相同的类型,与 ICommand<{}> 相同的类型。是的,它们都是不同的名称,但是类型系统不是名义上的,所以这并不重要。
您不能依赖于类型推断在这种情况下工作。当您调用 execute() 时,编译器尝试推断出 ICommand<T> 中的 T 的类型,但它没有任何可以推断的内容。因此,它最终默认为空类型 {}
解决此问题的方法是以某种方式使 ICommand<T> 在结构上依赖于 T,并确保实现 ICommand<Something> 的任何类型都正确执行此操作。根据您的示例代码,一种方法是:
interface ICommand<T> { 
  id: T;
}

因此,ICommand<T> 必须具有类型为 Tid 属性。幸运的是,GetSomethingByIdCommand 实际上确实具有类型为 stringid 属性,符合 implements ICommand<string> 的要求,所以它可以编译通过。

重要的是,您确实想要进行的推断会发生:

// goodResult is inferred as string even without manually specifying T
let goodResult = bus.execute(new GetSomethingByIdCommand('1'))

好的,希望能帮到你;祝你好运!

很棒的答案。如果你来自另一种支持泛型的语言,比如Java,这显然不是很直观。 - Jake Holzinger
此外,如果您处于无法依赖标签属性(在上面的示例中为 id)具有值的情况下,您可以将其定义为成员,并使用明确赋值运算符来避免必须设置它:id!: string - y2bd
@y2bd,你能再解释一下吗?我在接口中使用它时遇到了问题,所以最终使用了id?: string,这也适用于类实现。 - Shawn Mclean
1
@ShawnMclean 这是一个例子:https://repl.it/repls/WoozyStupidAtoms - y2bd

2

如果在传递给ICommandBus.execute()之前将具体类型强制转换为其通用等效类型,Typescript似乎能够正确推断类型:

最初的回答:

如果在传递给ICommandBus.execute()之前将具体类型强制转换为其通用等效类型,Typescript可以正确地推断类型。

let command: ICommand<string> = new GetSomethingByIdCommand('1')
let badResult = bus.execute(command)

或者:

let badResult = bus.execute(new GetSomethingByIdCommand('1') as ICommand<string>)

这并不是一种优雅的解决方案,但它可以工作。显然typescript泛型功能不是很完整。

原始答案: Original Answer


1

TS无法推断方法实现的接口与您想要的方式相符。

这里发生的情况是,当您使用以下代码实例化一个新类时:

new GetSomethingByIdCommand('1') 

实例化一个新类的结果是一个对象。因此,execute<T>会返回一个对象,而不是你期望的字符串。
在执行函数返回结果后,您需要进行类型检查。
在对象和字符串之间,您可以只做typeof检查。
const bus = new CommandBus()
const busResult = bus.execute(new GetSomethingByIdCommand('1'));
if(typeof busResult === 'string') { 
    ....
}

这在运行时很好用,当typescript被编译为纯JS时。
对于对象或数组(也是对象:D),您将使用类型保护。
类型保护尝试将项目转换为某个内容,并检查属性是否存在并推断使用了哪个模型。
interface A {
  id: string;
  name: string;
}

interface B {
  id: string;
  value: number;
}

function isA(item: A | B): item is A {
  return (<A>item).name ? true : false;
}

2
这个问题实际上是在问如何使类型推断以一种直观的方式工作。由于execute()接收一个实现了ICommand<T>接口的对象,而且T被绑定为string,所以预期badResult应该是string类型的。 - Igor Soloydenko
很遗憾,在这种情况下,TS推断有点受限。你不能以那种方式推断已实现的接口。实例化一个新类的结果始终是一个对象,因此返回类型被推断为对象。 - Vlatko Vlahek

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