Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
254 views
in Technique[技术] by (71.8m points)

typescript - How to define Map with correlation between a key type and a value type, while they both are unions

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

  1. Set does not show an error on incorrect code
  2. Immer Draft type (or any DeepWritable utility type) completely messes up this trick

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
question from:https://stackoverflow.com/questions/65891135/how-to-define-map-with-correlation-between-a-key-type-and-a-value-type-while-th

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


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.

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

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.

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...