来自接口实现的 Typescript 泛型推断

Posted

技术标签:

【中文标题】来自接口实现的 Typescript 泛型推断【英文标题】:Typescript generic inference from interface implementation 【发布时间】:2019-08-12 20:33:00 【问题描述】:

我试图从传入的参数的泛型推断方法的返回类型。但是,参数是通用接口的实现,所以我假设打字稿推断会根据参数的类型确定类型基地。

示例代码:

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 推断正确的返回值,在这种情况下是 string 基于实现 GetSomethingByIdCommand 的内容。

我尝试过使用conditional types,但不确定这是否是一个解决方案或如何应用它。

【问题讨论】:

也许这里的问题是你的ICommand&lt;T&gt; 接口对于T 的使用方式没有任何限制。因此,您的GetSomethingByIdCommand也隐式实现 ICommand&lt;number&gt;(例如)。实际上,该类似乎是ICommand&lt;T&gt; 对于任何类型T 的实现。鉴于此,TypeScript 如何选择要推断的类型? @CRice 我不太明白这一点。 GetSomethingByIdCommand 在显式实现ICommand&lt;string&gt; 时如何隐式实现ICommand&lt;number&gt;?你能详细说明一下吗? 【参考方案1】:

您的问题是ICommand&lt;T&gt; 在结构上不依赖于T(如@CRice 的评论中所述)。

这是not recommended。 (⬅ 指向 TypeScript 常见问题解答条目的链接,详细说明了一个与此案例几乎完全相同的案例,因此这与我们可能会在这里得到的官方词一样接近)

TypeScript 的类型系统(大部分)是结构性的,而不是名义上的:当且仅当它们具有相同的形状(例如,具有相同的属性)并且它们是否具有相同的名称时,它们才是相同的.如果ICommand&lt;T&gt;在结构上 依赖于T,并且它的所有属性都与T 无关,那么ICommand&lt;string&gt;相同的类型ICommand&lt;number&gt;,与ICommand&lt;ICommand&lt;boolean&gt;&gt;同类型,与ICommand&lt;&gt;同类型。是的,这些都是不同的名称,但是类型系统不是名义上的,所以这并不重要。

在这种情况下,您不能依赖类型推断来工作。当您调用execute() 时,编译器会尝试在ICommand&lt;T&gt; 中推断T 的类型,但它无法从中推断出任何东西。所以它最终默认为空类型

解决此问题的方法是使ICommand&lt;T&gt; 在结构上以某种方式依赖于T,并确保实现ICommand&lt;Something&gt; 的任何类型都正确执行此操作。给定您的示例代码,一种方法是:

interface ICommand<T>  
  id: T;

所以ICommand&lt;T&gt; 必须具有id 类型的T 属性。幸运的是,GetSomethingByIdCommand 实际上确实有一个string 类型的id 属性,这是implements ICommand&lt;string&gt; 所要求的,所以编译得很好。

而且,重要的是,你想要的推理确实发生了:

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

好的,希望对您有所帮助;祝你好运!

【讨论】:

很好的答案。如果您来自支持泛型的不同语言(例如 Java),这肯定不是很直观。 另外,如果您处于不能依赖为您的标签属性赋值的情况(在上面,id),您可以将其定义为成员并使用明确的分配运算符以避免必须设置它:id!: string; @y2bd 你能解释一下吗?我在接口中使用它时遇到了问题,所以我最终使用了id?: string,它也适用于类实现。 @ShawnMclean 这里是一个例子:repl.it/repls/WoozyStupidAtoms【参考方案2】:

如果具体类型在传递给 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 泛型的功能不是很完整。

【讨论】:

【参考方案3】:

TS 无法以您希望的方式推断该方法正在实现的接口。

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

new GetSomethingByIdCommand('1') 

实例化一个新类的结果是一个对象。因此,execute&lt;T&gt; 将返回一个对象,而不是您期望的字符串。

您需要在执行函数返回结果后进行类型检查。

在对象 vs 字符串的情况下,你可以做一个 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;

【讨论】:

问题真正要问的是如何使类型推断以直观的方式工作。 badResult 应该有 string 类型,因为 execute() 被赋予了一个实现 ICommant&lt;T&gt; 的对象,而 T 绑定到 string... 不幸的是,TS 推理在这种情况下有点受限。您不能以这种方式推断实现的接口。实例化一个新类的结果总是一个对象,因此返回类型被推断为一个对象。

以上是关于来自接口实现的 Typescript 泛型推断的主要内容,如果未能解决你的问题,请参考以下文章

在 TypeScript 中推断泛型函数类型

为啥 TS 中的泛型接口不能正确推断类型?

Typescript 推断的泛型类型在条件类型中是未知的

在泛型接口中推断变量类型

如何使泛型 TypeScript 接口从另一个泛型接口扩展?

Typescript 类不实现接口类型