两个相同的 TypeScript 接口具有不同的名称 - 如何使其干燥/避免重复?

Two identical TypeScript interfaces with different names - how to make it DRY / avoid repetition?

提问人:GuillaumeNo 提问时间:11/1/2023 更新时间:11/1/2023 访问量:39

问:

我正在开发一个金融应用程序,它有“出价”(买东西的提议)和“卖出价”(卖东西的提议)。

interface Ask {
  price: number
  amount: number
}

interface Bid {
  price: number
  amount: number
}

如何避免重复两个相同的类型定义?(它们将始终相同) 我只能有一个接口并调用它,但它失去了意义和清晰度。Offer

我试过这个:

interface Offer {
  price: number
  amount: number
}

interface Bid extends Offer {}
interface Ask extends Offer {}

但是我收到一个 eslint 错误,如果我不向它添加一些东西,扩展界面就没有意义了。An interface declaring no members is equivalent to its supertype.

打字稿 类型

评论

1赞 Parzh from Ukraine 11/1/2023
我认为你应该在任何地方定义和使用。如果 和 与 没有什么不同,将它们(即使使用类型别名)定义为单独的类型可能会让很多人(甚至是未来的你)感到困惑。OfferOfferBidAskOffer
0赞 GuillaumeNo 11/1/2023
谢谢!但令人困惑的是,我们的用户都没有谈论报价,他们只谈论出价和要价。知道这一点,您还会推荐使用产品/服务吗?Offer
1赞 Parzh from Ukraine 11/1/2023
“提供”这个词可能会令人困惑,但原则仍然存在:定义和使用一件事(实际上看起来像一个事物的两个名称,也许?当我难以找到一个好词时,GPT(ChatGPT、Bing AI 等)通常非常有帮助。向它描述这个想法,并要求一个词来形容它。BidAskTransaction
1赞 T.J. Crowder 11/1/2023
@GuillaumeNo - 仅供参考,我已经更新了下面的答案,以显示使用“喜欢”的类型。这是无品牌和品牌之间的一个很好的中间地带。

答:

3赞 3 revsT.J. Crowder #1

您可以使用类型别名:

interface Ask {
    price: number;
    amount: number;
}

type Bid = Ask;

游乐场链接

请注意,由于 TypeScript 的类型系统是结构化的(基于类型的形状),而不是名词(基于类型的名称/标识),因此具有该类型的对象将可以分配给变量/属性/参数,反之亦然。TypeScript 不区分它们:AskBid

let a: Ask = { price: 42, amount: 2 };
//  ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4 };
//  ^? −−− let b: Ask
a = b; // <== This is allowed

请注意,显示的类型提示是 ,而不是 。bAskBid

如果你想保持这两种类型之间的区别,你可以使用 Drew Colthorp 在 Flavoring: Flexible Nominal Typing for TypeScript 一文中描述的技术(感谢 VLAZ 指出这一点;另请参阅 VLAZ 对相关问题的回答)。它的核心是,你可以通过使用可选的品牌属性来区分类型,如下所示(但请继续阅读):

interface Offer {
    price: number;
    amount: number;
}

interface Bid extends Offer {
    __type__?: "Bid";
}

interface Ask extends Offer {
    __type__?: "Ask";
}

由于该属性是可选的,因此在创建实例时不必指定它,但它会使类型彼此不兼容(尽管您可以轻松地从一个类型转换为另一个类型):

let a: Ask = { price: 42, amount: 2 };
//  ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4 };
//  ^? −−− let b: Bid
a = b; // <== Error: Type 'Bid' is not assignable to type 'Ask'.
       //              Types of property '__type__' are incompatible.
       //                Type '"Bid" | undefined' is not assignable to type '"Ask" | undefined'.(2322)

游乐场链接

在本文中,Colthorp 通过创建一个可重用的泛型类型来进一步发展:Flavor

interface Flavoring<FlavorT> {
    _type?: FlavorT;
}
type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;

您将在代码中使用,如下所示:

interface Offer {
    price: number;
    amount: number;
}

type Bid = Flavor<Offer, "Bid">;

type Ask = Flavor<Offer, "Ask">;

游乐场链接

与 和 相同的测试表明,您不能将一个分配给另一个。ab

最后,您可以通过在实例中实际包含 (or ) 属性而不是使它们成为可选属性来使用完全品牌类型。我发现有时这样做很方便,因为我可以在调试器中看到这些属性,并且可以在处理实例的代码中使用它们来缩小类型。但有时你不希望额外的属性使对象混乱,因此采用了上面的“风味”方法。完整品牌示例:_type__type__Offer

interface Offer {
    price: number;
    amount: number;
}

interface Bid extends Offer {
    __type__: "Bid";
}

interface Ask extends Offer {
    __type__: "Ask";
}

let a: Ask = { price: 42, amount: 2, __type__: "Ask" };
//  ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4, __type__: "Bid" };
//  ^? −−− let b: Bid
a = b; // <== Error: Type 'Bid' is not assignable to type 'Ask'.
       //              Types of property '__type__' are incompatible.
       //                Type '"Bid"' is not assignable to type '"Ask"'.(2322)

游乐场链接

评论

2赞 VLAZ 11/1/2023
"缺点是,您必须真正拥有该属性(如上所述),或者在创建具有这些类型的对象时最初使用类型断言。还有一种方法可以使用我在这里的答案中描述的内容来减少品牌的侵入性(我没有想出,但阅读了一篇关于 - 链接在那里的文章)。这提供了类似的保护,防止交叉分配两者,但它更轻量级:只是工作,而不必关注品牌 tsplay.dev/mLrO2wlet a1: Ask = { price: 42, amount: 2};
1赞 T.J. Crowder 11/1/2023
谢谢,太美了!令人恼火的是,我以前见过它,但不记得了。你怎么看,我应该编辑它,还是你想发布一个答案,或者(可以说)我们应该将问题标记为重复的问题?
1赞 VLAZ 11/1/2023
我不认为这真的是重复的。解决方案恰好有效,但我不认为这是唯一的方法。我认为你的答案很好地涵盖了所有内容,我不觉得添加另一个答案是值得的。如果您愿意,您可以为调味品/不透明类型添加注释。仅此而已的答案将有点贫血:)
1赞 T.J. Crowder 11/1/2023
@VLAZ - 我也是这么想的,谢谢。再次感谢您指出这一点!