1

I would like to have a type that ensures an object has a type of A or B or A and B. However one of the cases that I think should fail is not. I'm sure it's something stupid I just can't see it yet.

interface ValueSelector
{
    type: "id" | "value_string"
    value: string
}

interface TemporalSelector
{
    id: number
}

type Selector = (ValueSelector & TemporalSelector) | ValueSelector | TemporalSelector

// Should error
const e0: Selector = {}
const e1: Selector = { id: 0, value: "" }  // <-- does not error
const e2: Selector = { type: "id" }
const e3: Selector = { type: "value_string" }
const e4: Selector = { value: "" }
const e5: Selector = { value: "" }

// Should pass
const a1: Selector = { id: 0 }
const a2: Selector = { type: "id", value: "" }
const a3: Selector = { type: "value_string", value: "" }
const a4: Selector = { id: 0, type: "id", value: "" }
const a5: Selector = { id: 0, type: "value_string", value: "" }
AJP
  • 26,547
  • 23
  • 88
  • 127

1 Answers1

2

e1 does not trigger an error because { id: 0, value: "" } is already assignable to TemporalSelector since it expects only id property.

In order to make it work you can use the StrictUnion helper:

interface ValueSelector {
    type: "id" | "value_string"
    value: string
}

interface TemporalSelector {
    id: number
}

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>


type Selector = (ValueSelector & TemporalSelector) | StrictUnion<ValueSelector | TemporalSelector>

// Should error
const e0: Selector = {}
const e1: Selector = { id: 0, value: "" }  // error
const e2: Selector = { type: "id" }
const e3: Selector = { type: "value_string" }
const e4: Selector = { value: "" }
const e5: Selector = { value: "" }

// Should pass
const a1: Selector = { id: 0 }
const a2: Selector = { type: "id", value: "" }
const a3: Selector = { type: "value_string", value: "" }
const a4: Selector = { id: 0, type: "id", value: "" }
const a5: Selector = { id: 0, type: "value_string", value: "" }

Playground

AJP
  • 26,547
  • 23
  • 88
  • 127
  • 1
    This is great. Thank you. I usually forget this Liskov Substitution Principle/rule. The `StrictUnion` is exactly what I need. The `tag` property will not work however as they allow `A` or `B` but not `A` or `B` or `A and B`. – AJP Oct 06 '21 at 22:50
  • 1
    Thank you. Also I did not think about Liskov SP in this context. Nice – captain-yossarian from Ukraine Oct 07 '21 at 05:15