Evgeny Timoshenko
Evgeny Timoshenko

Reputation: 3259

Inconclusive error message when using union type in typescript

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

Upvotes: 0

Views: 61

Answers (2)

Evgeny Timoshenko
Evgeny Timoshenko

Reputation: 3259

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

Upvotes: 0

MEDZ
MEDZ

Reputation: 2295

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.

Upvotes: 1

Related Questions