0
votes

I'm trying to describe a union type of a Firestore value:

interface StringValue {
    stringValue: string;
}

interface BooleanValue {
    booleanValue: boolean;
}

type ValueType = StringValue | BooleanValue;
var value: ValueType = { booleanValue: false, stringValue: "null" }; // [1]

if (value.booleanValue) console.log(value); // [2]

I'm getting an error at [2]:

Property 'booleanValue' does not exist on type 'ValueType'.
  Property 'booleanValue' does not exist on type 'StringValue'.(2339)

though I'd expect to error at line [1], where is an incorrect assignment happens. So, why [2] and not [1]?

https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=13&pc=1#code/JYOwLgpgTgZghgYwgAgMpiqA5gNTgGwFcUBvAWAChlrkBnDbPIiALjoZCwG5KBfSyqEixEKAEIB7CfghwQTYsnJUaAIyky5C1snXTZIHhX4VKYAJ4AHFNoAqVlAF40HXAUUAfZJP1b3EIwA3OChkYOY2OwdkZxJdDQNtNnh8WggAGnZMTiTkACIQQnx8POReI0EYZAAKcOIAOj1NeX8ASmQECRBafXr8CSxatqMgA

2
It's not really an incorrect assignment... your value is of type StringValue & BooleanValue, which is assignable to StringValue | BooleanValue (and not vice versa). Unions are generally inclusive, not exclusive. If you're looking for an "exclusive union" then maybe this is a duplicate. - jcalz
@jcalz it's very helpful, but I guess that XOR type is going to work only with a couple of types. - Evgeny Timoshenko

2 Answers

1
votes

It's a better practice to use in when you try to check if a property exists in an object:

if ('booleanValue' in value)
    console.log(value);

This check does not generate an error.

On the other hand, it did not generate an error for the assignment because Typescript checks for minimum properties to exist in an object but if you added more items TS will be Ok with that. Check this part of TS docs for more info.

0
votes

I've come up with a somewhat sufficient thing:

// makes all props undefined
type Undefined<T> = {
    [P in keyof T]: undefined;
};

type Values = {
    stringValue: string;
    booleanValue: boolean;
}

// picks Key prop from the Dict and marks the rest of them as undefined
type OnlyOne<Dict, Key extends keyof Dict> = Partial<Omit<Undefined<Dict>, Key>> & Pick<Dict, Key>

type StringValue = OnlyOne<Values, 'stringValue'>
type BooleanValue = OnlyOne<Values, 'booleanValue'>

var x: StringValue = { stringValue: 'q' }
var y: BooleanValue = { booleanValue: false }
var shouldFail: BooleanValue = {stringValue: 'a string', booleanValue: false} // fails


type ValueType = StringValue | BooleanValue;
var badValue1: ValueType = { booleanValue: false, stringValue: "null" }; // fails
var badValue2: ValueType = { foo: 'bar' }; // fails
var okValue1: ValueType = { booleanValue: true };
var okValue2: ValueType = { stringValue: 'string' };

if (okValue1.booleanValue) {
    var bv: boolean = okValue1.booleanValue; // ok here
    var sv: string = okValue1.stringValue; // fails, stringValues is undefined
    console.log(okValue1.stringValue);
}

playground


Here is what would work without using Xor helper type from the related question:

interface StringValue {
    kind: 'string',
    stringValue: string;
}

interface BooleanValue {
    kind: 'boolean',
    booleanValue: boolean;
}

type ValueType = StringValue | BooleanValue;
var value: ValueType = { booleanValue: false, stringValue: "null", kind: 'string' }; // [1]

if (value.booleanValue) console.log(value); // [2]

so by extending StringValue and BooleanValue with a common kind field, it errors at both [1] and [2].

https://www.typescriptlang.org/play/index.html?ssl=12&ssc=82&pln=12&pc=68#code/JYOwLgpgTgZghgYwgAgMpiqA5gNTgGwFcUBvAWAChlrkBrUAEwC5kByAZw21YBpKbknTCFwFiLIdgDclAL6VKoSLEQoAQgHsN+CHBB4ipfjXohmbAEZade3sepXtu-WIgtHNkDIryKlMACeAA4oBsQAKsEoALxoXCJhKAA+yJpOeoneAG5wUMg5hiyJkSHIsSTIHs6JLPD47BA8gvGihcgARCCE+PjtTabmHC2syLLeijDIABQFxAB0VRmuAJTICBog7E5z+BpYMyveQA


Another way is to use XOR helper type from the answer