Discriminated unions with booleans in Typescript

Viewed 827

To keep track of an initializing/initialized object I want to create a discriminated union with a boolean. And so I wrote the following code:

interface InitializingThing extends BaseThing {
    initialized: false;
    value: undefined;
}

interface InitializedThing extends BaseThing {
    initialized: true;
    value: number;
}

type Thing = InitializingThing | InitializedThing;

const thing: Thing = { initialized: false, value: undefined };
console.log(thing);

getThing().then((value: number) => {
    thing.value = value;
    thing.initialized = true;
}).then(() => {
    if (!thing.initialized) {
        return;
    }
    console.log(15 + thing.value);
});

(see on the Typescript playground )

However this gives the errors

Type 'number' is not assignable to type 'undefined'.(2322)
Type 'true' is not assignable to type 'false'.(2322)

I can see from hovering over the console.log(thing) that the type is InitializingThing instead of Thing! Which seems to be the root of the problem, but I'm not sure why the TS compiler would do this.

2 Answers

Your code in principle works fine. However, in above example, TS actually is a bit "too smart" with its control flow analysis. In following variable assignment

const thing: Thing = { initialized: false, value: undefined };

, the compiler narrows the type of thing to InitializingThing by interpreting its initialiser. thing is also assumed to keep this type, since the variable is readonly/const. That is why an error is triggered for the re-assignments in the then clause.

If you enforce thing to really be of type Thing, given sample will compile again:

const thing: Thing = { initialized: false, value: undefined } as Thing; // note the cast here
// or 
let thing: Thing = { initialized: false, value: undefined }; // switch to mutable `let`

Playground

At this line of code, the object is invalid:

thing.value = value;

It now has the type {initialized: false, value: number}, which is not correct.

Instead, you need to change the values together:

thing = { value: value, initialized, true }

But to do that, you'll find that thing cannot be const (because you're assigning a new value to it). You'll need to make it let.

Related