0

When I have a type that can be something or undefined, I can easily use an if statement to check it contains something:

type MaybeString = string | undefined;

function useMaybeString(arg: MaybeString) {
    if (arg !== undefined) {
        printString(arg);
    }
}

function printString(arg: string) {
    console.log(arg)
}

But this example where my type can be one of two posibilities doesn't work:

type Foo = {foo: string};
type Bar = {bar: string};

type FooOrBar = Foo | Bar;

function useFooOrBar(arg: FooOrBar) {
    if (arg.foo !== undefined) {
        console.log(arg.foo);
    } else {
        console.log(arg.bar);
    }
}

What is the solution? And why does TypeScript complain?

Josu Goñi
  • 1,178
  • 1
  • 10
  • 26
  • 1
    Try `if('foo' in arg) { ... } else { ... }`. Typescript won't allow you to access a property that might not exist, but if you check it exists first then there is no problem. – kaya3 May 30 '21 at 20:24
  • @kaya3 Perfect! If you write it as an answer I will mark it as correct. – Josu Goñi May 30 '21 at 20:29
  • 1
    Side-note, beware of unions like yours, where `{foo: 5, bar: 'baz'}` is a possible value because it's assignable to `{bar: string}`, but `'foo' in arg` will be true. It's better if you have a discriminant, like `type FooOrBar = {kind: 'foo', foo: string} | {kind: 'bar', bar: string}` so the union branches are mutually exclusive; then when you test the discriminant, there is no ambiguity. – kaya3 May 30 '21 at 20:31
  • 1
    I believe this answer will be useful https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties – captain-yossarian from Ukraine May 30 '21 at 20:32

2 Answers2

1

On this line

if (arg.foo !== undefined) {

it complains, because only Foo has the foo property, Bar doesn't have it.

So, at this point, we can't access arg.foo because we don't yet know if arg is Bar or Foo.

Solution

I think you could use User-defined type guards:

const isFoo = (maybeFoo: FooOrBar): maybeFoo is Foo => 
  (maybeFoo as Foo).foo !== undefined;

Then you can use this to assert the type:

function useFooOrBar(arg: FooOrBar) {
  if (isFoo(arg)) {
    console.log(arg.foo);
  } else {
    console.log(arg.bar);
  }
}
rsmeral
  • 518
  • 3
  • 8
0

One of the ways is to explicitly include the other properties into both types, but set them as optional and undefined.

type Foo = {
    foo: string;
    bar?: undefined;
};

type Bar = {
    foo?: undefined;
    bar: string;
};

type FooOrBar = Foo | Bar;

function useFooOrBar(arg: FooOrBar) {
    if (arg.foo !== undefined) {
        console.log(arg.foo);
    } else {
        console.log(arg.bar);
    }
}

Demo in TypeScript playground.

You can also just tell the compiler that the property is there by using Type Guards. However, you will need to check it for every condition, which can be quite cumbersome:

type Foo = {foo: string};
type Bar = {bar: string};

type FooOrBar = Foo | Bar;

function useFooOrBar(arg: FooOrBar) {
    if ('foo' in arg && arg.foo !== undefined) {
        console.log(arg.foo);
    } else if ('bar' in arg && arg.bar !== undefined) {
        console.log(arg.bar);
    }
}

Demo in TypeScript playground.

yqlim
  • 6,898
  • 3
  • 19
  • 43
  • After `'foo' in arg` I think there is no need for the undefined check. There is also no need for the `else if` since TypeScript already knows there is only one option remaining. – Josu Goñi May 30 '21 at 20:33