7

I want to create a Discriminated Union Type, where it isn't required to pass the discriminator value.

Here's my current code:

interface Single<T> {
  multiple?: false // this is optional, because it should be the default
  value: T
  onValueChange: (value: T) => void
}

interface Multi<T> {
  multiple: true
  value: T[]
  onValueChange: (value: T[]) => void
}

type Union<T> = Single<T> | Multi<T>

For testing I use this:

function typeIt<T>(data: Union<T>): Union<T> {
    return data;
}

const a = typeIt({ // should be Single<string>
    value: "foo",
    onValueChange: (value) => undefined // why value is of type any?
})

const b = typeIt({ // should be Single<string>
    multiple: false,
    value: "foo",
    onValueChange: (value) => undefined
})

const c = typeIt({ // should be Multi<string>
    multiple: true,
    value: ["foo"],
    onValueChange: (value) => undefined
})

But I get a bunch of errors and warnings...:

  1. In const a's onValueChange the type of the parameter value is any. When setting multiple: false explicitly (like in const b) it gets correctly inferred as string.

  2. const c doesn't work at all. I get this error: "Type 'string' is not assignable to type 'string[]'".

Do you have any idea how to solve this?

I've created a TypeScript Playground with this code

Benjamin M
  • 23,599
  • 32
  • 121
  • 201
  • If you are using generic types, then you should specify the type, it also helps other developers. – Pavlo Jan 03 '19 at 23:14
  • 1
    @Pavlo I could not disagree more. The power of TS is the ability to infer types (and I have inferred some highly complex types). Plus explicitly specifying the type basically duplicates the information which can be especially painful if the type is not explicitly named – Titian Cernicova-Dragomir Jan 04 '19 at 06:46
  • 1
    It may be worth noting that 1 is documented [here](https://github.com/microsoft/TypeScript/issues/41759) and 2 is fixed in version 3.6 – Erik Apr 04 '21 at 05:02

1 Answers1

5

I don't think the compiler can easily infer the type of the value parameter in the callback since the type of the object literal is still not determined when the callback is checked.

If you don't have a lot of union members a solution that works as expected is to use multiple overloads:

export interface Single<T> {
  multiple?: false // this is optional, because it should be the default
  value: T
  onValueChange: (value: T) => void
}

interface Multi<T> {
  multiple: true
  value: T[]
  onValueChange: (value: T[]) => void
}

type Union<T> = Single<T> | Multi<T>

function typeIt<T>(data: Single<T>): Single<T>
function typeIt<T>(data: Multi<T>): Multi<T>
function typeIt<T>(data: Union<T>): Union<T> {
    return data;
}

const a = typeIt({ // is Single<string>
    value: "foo",
    onValueChange: (value) => undefined // value is typed as expected
})

const b = typeIt({ // is Single<string>
    multiple: false,
    value: "foo",
    onValueChange: (value) => undefined
})

const c = typeIt({ // is be Multi<string>
    multiple: true,
    value: ["foo"],
    onValueChange: (value) => undefined
})
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357