TypeScript 声明合并 - 更改属性类型

Posted

技术标签:

【中文标题】TypeScript 声明合并 - 更改属性类型【英文标题】:TypeScript declaration merging - changing a property type 【发布时间】:2017-09-10 00:10:00 【问题描述】:

是否可以更改在接口中声明的属性的类型?我不知道如何用语言来解释这一点,所以这里有一个简化的例子,说明我正在尝试做的事情不起作用:

假设Movie 命名空间是在我无法控制的第 3 方库中定义的:

declare namespace Movie 
  interface Character 
    doSomething(): Promise<void>;
    friend: Character;
  

现在在我的应用程序中,我希望我的角色是 Super,有 Super 朋友。所以我尝试在自己的打字中做到这一点:

declare namespace Movie 
  interface Character 
    doAnExtraThing(): Promise<void>; // OK to add a new method
    nickname: string;                // OK to add a new property
    friend: SuperCharacter;          // NOT OK to override type with a subtype
  

  interface SuperCharacter extends Character 
    doSomethingSuper(): Promise<void>;
  

但这不起作用,因为 TypeScript 不允许我用 SuperCharacter 覆盖 friend 属性类型,即使根据定义 SuperCharacter 一个Character。 TypeScript 报错:

[ts] Subsequent variable declarations must have the same type. Variable
'friend' must be of type 'Character', but here has type 'SuperCharacter'.

我希望通过让SuperCharacter 扩展原来的Character 接口我不会遇到这个问题,但我确实遇到了。

希望很清楚我想要达到的目标。有办法吗?

【问题讨论】:

【参考方案1】:

没有理由明确指定friend 可以是SuperCharacter,因为SuperCharacter 已经是Character

所以你可以完全删除这个块:

界面字符 朋友:超级角色;

作为证明,我们可以离开接口,看看一个可以正确编译的类的实现:

class Character 
  friend?: Character

  constructor(friend?: Character) 
    this.friend = friend
  

  doSomething() 


class SuperCharacter extends Character 
  doSomethingSuper() 



new Character(new Character())
new Character(new SuperCharacter())
new SuperCharacter(new Character())
new SuperCharacter(new SuperCharacter())

【讨论】:

不幸的是,这不适用于我的用例。就像我在原帖中所说的,我希望我的角色是超级的,有超级的朋友,这意味着我应该能够做到const character = new Character(),然后是character.doSomethingSuper()character.friend.doSomethingSuper()。跨度> 在这种情况下,您需要实例化一个 SuperCharacter 以开始:const character = new SuperCharacter()。来自子类的方法(例如 doSomethingSuper())永远不会出现在父类中! 我无法控制实例的创建方式。我的问题特别是关于如何通过声明合并将现有接口上的属性类型更改为新的 compatible 类型(与子类型一样)。可以向接口添加新属性,但似乎无法更改属性的类型,即使新类型与原始类型兼容。 但是,在您的示例中,您并没有向现有界面添加新属性,而是创建了一个扩展现有界面的全新界面。同样重要的是要注意这两种类型并不真正兼容。 SuperCharacter 是 Character 的一种,但 Character 不是 SuperCharacter。使这两种类型匹配的唯一方法是直接扩展 Character 以包含 doSomethingSuper() 方法。 我的意思是可能向现有接口引入全新的属性,而不是我需要的。我已经更新了这个例子来展示这是怎么可能的。但是,我需要的是覆盖一个属性类型。正如您所说,SuperCharacter 仍然 Character,因此应该能够在需要Character 的任何地方提供SuperCharacter(而不是相反)。到目前为止,这似乎只是 TypeScript 编译器的一个限制,而我想要做的事情根本不可能。感谢您提供帮助。【参考方案2】:

我想出了两个解决方案:

解决方案 1

您也可以使用联合类型和条件类型来完成与 pirix-gh 的答案相同的结果。

首先我们将两种类型合并在一起:

type Merge&lt;T, U&gt; = T &amp; U

那么我们可以使用条件类型来改变字段友:

type ChangeProperty<T, K extends keyof T, U> = 
    [Prop in keyof T]: Prop extends K ? U : T[Prop]

要产生所需的类型,像这样使用它:

// From the namespace
interface Character 
    doSomething(): Promise<void>;
    friend: Character;


interface SuperCharacter extends Character 
    doSomethingSuper(): Promise<void>;


type MyCharacter =  ChangeProperty<Merge<SuperCharacter, SuperCharacter>, "friend", SuperCharacter>

使用这种方法,您不必向 SuperCharacter 添加字段,您可以重复使用 friend 字段并将其类型更改为 SuperCharacter

解决方案 2

这是基于 pirix-gh 的回答,但没有库。还使用联合和条件类型。

type SuperMerge<T, U> = 
    [K in keyof (T & U)]: K extends keyof T ? T[K] : K extends keyof U ? U[K] : never

SuperMerge 类型结合了 T 和 U,然后遍历它的 props,然后当属性在 T 中时,它使用 T 中的类型,否则它将使用 U 中的类型。 never 部分永远不会发生。

你可以这样使用它:

interface SuperCharacter extends Character 
    doSomethingSuper(): Promise<void>;
    friend: SuperCharacter // You need to this line in order to overwrite the default

使用此解决方案,您需要将具有新类型的字段朋友添加到界面中以覆盖默认值。第一种类型包含所有前导属性并将被保留,因此如果第二种类型中有任何重复,则信息将被第一种覆盖。第二种类型的唯一功能是用额外的属性扩展第一种类型。

type MyCharacter = SuperMerge&lt;SuperCharacter, Character&gt;

结论

两种解决方案输出相同的结果:

type MyCharacter = 
    doSomethingSuper: () => Promise<void>;
    doSomething: () => Promise<void>;
    friend: SuperCharacter;

为了选择一个,我认为两者各有利弊。当您想要合并您无法控制的 2 种类型时,第一个解决方案很有用,但是您一次只能更改一种类型。您可以递归调用 useChangeType 来更改更多属性。

使用解决方案 2,您可以将新属性添加到新类型,并且所有具有相同名称的属性都将被第一种类型覆盖。

通过这两种解决方案,您可以与自己的类型合并。

【讨论】:

【参考方案3】:

您可以通过ts-toolbelt 实现此目的。但是,您将拥有一个type,而不是一个界面:

import O from 'ts-toolbelt'

interface Character 
    doSomething(): Promise<void>
    friend: Character
  

interface SuperCharacter 
    doSomethingSuper(): Promise<void>;
    friend: SuperCharacter;


// SuperCharacter will override Character  
type SuperMerge = O.Merge<SuperCharacter, Character>

// type SuperMerge = 
//     doSomethingSuper: () => Promise<void>;
//     friend: SuperCharacter;
//     doSomething: () => Promise<void>;
// 

然后做

interface SuperMergedCharacter extends SuperMerge 
 ...

【讨论】:

以上是关于TypeScript 声明合并 - 更改属性类型的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript声明合并

Typescript杂谈

TypeScript学习笔记

Ts高级类型(Utility Types)

TypeScript:预期类型来自在“IntrinsicAttributes & Prop”上声明的属性“children”

如何仅使用 TypeScript 声明其属性为字符串类型的对象?