2

I have three different types

type A = 'a'
type B = 'b'
type C = 'c'

I want to type a function either accepts A, C or B, C but not A, B, and C.

Here is my attempt

type A = 'a'
type B = 'b'
type C = 'c'
type BaseArgs = {
    c: C
}
type VariantA = {
    a: A
} & BaseArgs

type VariantB = {
    b: B,
} & BaseArgs

function fn(arg: VariantA | VariantB) {
}

But turns out it doesn't work as expected, as

const b: B = 'b'
const a: A = 'a'
const c: C = 'c'

fn({b,a,c})  // this would not error out

fn({b,a,c}) should be giving an error but it is not.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Joji
  • 4,703
  • 7
  • 41
  • 86

3 Answers3

1

Seems like TypeScript is being nice here--the object is both a valid VariantA or VariantB so it allows it, despite having an extra variable if you were to "choose" just one or the other. If you don't want this, you could specify explicitly:

type VariantA = {
    a: A;
    b?: never; // This line!
} & BaseArgs

(and do the same for VariantB)

DemiPixel
  • 1,768
  • 11
  • 19
1

{a:A, b:B, c:C} matches the union of your acceptable types.

use an overload to specify you only want one or the other:

function fn(arg: VariantA): void;
function fn(arg: VariantB): void;
function fn(arg: VariantB | VariantA) {
}

typscript playground

bryan60
  • 28,215
  • 4
  • 48
  • 65
1

In order to achieve desired behavior, you should wrap your union type in StrictUnion helper:

// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
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 A = 'a'
type B = 'b'
type C = 'c'

type BaseArgs = {
    c: C
}

type VariantA = {
    a: A
} & BaseArgs

type VariantB = {
    b: B,
} & BaseArgs

function fn(arg: StrictUnion<VariantA | VariantB>) {}


const b: B = 'b'
const a: A = 'a'
const c: C = 'c'

fn({ a, c }) // ok
fn({ b, c }) // ok

fn({ b, a, c })  // this would not error out

Playground

You did not have an error here fn({ b, a, c }) because this argument is assignable to both union types.