如何定义具有键类型和值类型之间相关性的映射,而它们都是联合 [英] How to define Map with correlation between a key type and a value type, while they both are unions

查看:22
本文介绍了如何定义具有键类型和值类型之间相关性的映射,而它们都是联合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是显示我想要实现的目标的示例.除了两个问题外,它几乎可以工作:

Here is the example which shows what I want to achieve. It almost works except 2 problems:

  1. Set 不显示错误代码的错误
  2. Immer Draft 类型(或任何 DeepWritable 实用程序类型)完全搞砸了这个技巧

基本上在我看来,我在这里所做的并不是一件事.所以问题是:有没有其他方法可以做同样的事情?

Basically it seems to me that what I am doing here isn't really a thing. So the question is: is there some other way to do the same thing?

type Opaque<Type, Token = unknown> = Type & {
  readonly __opaque__: Token
}

type AnimalId = CatId | DogId

type Animal = Cat | Dog

type CatId = Opaque<number, Cat>

type Cat = {
  readonly kind: 'Cat'
  readonly id: CatId
}

type DogId = Opaque<number, Dog>

type Dog = {
  readonly kind: 'Dog'
  readonly id: DogId
}

const cat: Cat = {
    kind: 'Cat',
    id: 1 as CatId,
}

const dog: Dog = {
    kind: 'Dog',
    id: -1 as DogId,
}

const animals: Map<CatId, Cat> & Map<DogId, Dog> & Map<AnimalId, Animal> = new Map()

animals.set(cat.id, cat) // no error should be here

animals.set(cat.id, dog) // wanna see error here

const test1: Cat | undefined = animals.get(cat.id) // no error should be here

const test2: Dog | undefined = animals.get(dog.id) // no error should be here

const test4: Animal | undefined = animals.get(1 as AnimalId) // no error should be here

const test3 = animals.get(3) // wanna see error here

推荐答案

交集 Map&Map 应该概念上足以为您提供您想要的行为,但实际上这是行不通的.函数类型的交集会产生重载,TypeScript 中的重载调用签名是单独解决.它们不组合以允许单个调用调用两个调用签名(请参阅 microsoft/TypeScript#14107).所以只需 Map;&Map,不能调用animals.get(1 as AnimalId)AnimalId 既不是 CatId 也不是 DogId,这是两个单独的调用签名所要求的.

The intersection Map<CatId, Cat> & Map<DogId, Dog> should conceptually be enough to give you the behavior you want, but in practice this does not work. Intersections of function types produce overloads, and overloaded call signatures in TypeScript are resolved separately. They are not combined to allow a single call to invoke both call signatures (see microsoft/TypeScript#14107). So with just Map<CatId, Cat> & Map<DogId, Dog>, you cannot call animals.get(1 as AnimalId); an AnimalId is neither a CatId nor a DogId, as required by each of the two separate call signatures.

为了解决这个问题,您显然添加了失踪"地图.不幸的是,这太过分了.您不仅获得了理想的 get() 行为,还获得了不良 set() 行为.由于cat.id 是一个AnimalId,而dog 是一个Animal,一个Map 肯定会允许你调用 animals.set(cat.id, dog).我不会深入探讨协变和逆变的迂腐细节,但一般来说,如果阅读接受事物的联合,那么写作应该接受交集-of-things,不是工会.因此,您喜欢支持的Map的唯一方法是涉及阅读内容的方法.

In order to address this, you have apparently added in the "missing" Map<AnimalId, Animal>. Unfortunately this goes too far. You not only get the desirable get() behavior, you get the undesirable set() behavior. Since cat.id is an AnimalId, and dog is an Animal, a Map<AnimalId, Animal> would certainly allow you to call animals.set(cat.id, dog). I won't go into the pedantic details of covariance and contravariance, but generally speaking, if reading accepts unions-of-things, then writing should accept intersections-of-things, not unions. So the only methods of Map<AnimalId, Animal> you'd like to support are ones involving reading the contents.

对我们来说幸运的是,有一个 ReadonlyMap 在 TypeScript 标准库中定义的接口 正是为了这个目的.所以我倾向于像这样注释animals:

Fortunately for us, there is a ReadonlyMap interface defined in the TypeScript standard library which serves just this purpose. So I'd be inclined to annotate animals like this:

const animals: Map<CatId, Cat> & Map<DogId, Dog>
  & ReadonlyMap<AnimalId, Animal> = new Map();

一旦你这样做,你就会得到你正在寻找的行为,至少对于你的示例代码:

Once you do that, you get the behavior you're looking for, at least for your example code:

animals.set(cat.id, cat) // okay
animals.set(cat.id, dog) // error, no overload matches this call
const test1: Cat | undefined = animals.get(cat.id) // okay
const test2: Dog | undefined = animals.get(dog.id) // okay
const test4: Animal | undefined = animals.get(1 as AnimalId) // okay
const test3 = animals.get(3) // error, number is not AnimalId

当然,可能存在此定义不支持的其他用例.重载确实有一些奇怪的怪癖.如果您真的很在意,您可能需要手写自己的界面,该界面的行为与您期望的多类型 Map 的行为完全相同.这是我之前针对不同情况做过的有趣练习(参见 这个问题) 老实说,这并没有那么糟糕.但我不会在这里深入讨论,特别是如果上述更简单的交集适用于您的用例.

There may, of course, be other use cases that this definition does not support. Overloads do have some weird quirks. If you really truly care, you might need to handwrite your own interface that behaves exactly how you expect a multi-type Map to act. This is an interesting exercise I've done before for a different case (see this question) and honestly it's not that terrible. But I won't go into that here, especially if the above simpler intersection works for your use cases.

代码的游乐场链接

这篇关于如何定义具有键类型和值类型之间相关性的映射,而它们都是联合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆