如何定义具有键类型和值类型之间相关性的 Map,而它们都是并集

gyz*_*rok 3 typescript

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

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

基本上在我看来,我在这里所做的事情并不是什么真正的事情。所以问题是:还有其他方法可以做同样的事情吗?

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
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

从概念上讲,交集Map<CatId, Cat> & Map<DogId, Dog>应该足以为您提供所需的行为,但实际上这是行不通的。函数类型的交集会产生重载,并且 TypeScript 中的重载调用签名会单独解析。它们不会组合在一起以允许单个调用调用两个调用签名(请参阅microsoft/TypeScript#14107)。因此,使用 just 时,您无法调用; an既不是 a也不是 a ,正如两个单独的调用签名所要求的那样。Map<CatId, Cat> & Map<DogId, Dog>animals.get(1 as AnimalId)AnimalIdCatIdDogId


为了解决这个问题,您显然添加了“missing” Map<AnimalId, Animal>。不幸的是,这太过分了。你不仅会得到理想的get()行为,还会得到不良的 set()行为。既然cat.id是一个AnimalId,而且dog是一个Animal,一个Map<AnimalId, Animal>肯定会让你打电话animals.set(cat.id, dog)。我不会深入探讨协变和逆变的迂腐细节,但一般来说,如果阅读接受事物的并集,那么写作就应该接受事物的交集,而不是并集。Map<AnimalId, Animal>因此,您想要支持的唯一方法是涉及阅读内容的方法。

对我们来说幸运的是,ReadonlyMapTypeScript 标准库中定义了一个接口,专门用于此目的。所以我倾向于animals这样注释:

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

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

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
Run Code Online (Sandbox Code Playgroud)

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

Playground 代码链接