0

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

Evgeny Timoshenko
  • 3,119
  • 5
  • 33
  • 53
  • 1
    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](https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types). – jcalz Dec 19 '19 at 21:33
  • @jcalz it's very helpful, but I guess that `XOR` type is going to work only with a couple of types. – Evgeny Timoshenko Dec 20 '19 at 07:51

2 Answers2

1

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.

MEDZ
  • 2,227
  • 2
  • 14
  • 18
  • Thank you for the response. I'd like to find a way to avoid situations when an incorrect object is constructed. In the case of objects, both `&` and `|` have the same effect which looks odd to me. – Evgeny Timoshenko Dec 19 '19 at 18:12
0

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

Evgeny Timoshenko
  • 3,119
  • 5
  • 33
  • 53