Typescript: Accessing property in union of object types fails for properties not defined on all union members

Created on 10 Dec 2016  ·  12Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 2.1.1

Code

type A = { a: string; } 
type B = { b: string; }
type AorB = A | B;

declare const AorB: AorB;

if (AorB.a) {
    // use AorB.a
}

Expected behavior:
The code compiles without errors. Even though a doesn't (necessarily) exist on all the constituents of the union, the fact that it exists on some should allow me to check for .a and use it if it is present.

Actual behavior:
Typescript complains: "Property a does not exist on type AorB... Property a does not exist on type B."

Question

Most helpful comment

i find a solution on book :

interface A { x:  number;} 
interface B { y:  string;}
function doStuff ( q: A | B ) {
  if ( 'x'  in q) {
    //  if type A...
  }
  else {
    // if type B...
  }
}

Excerpt From: Basarat Ali Syed. “TypeScript Deep Dive.” Apple Books.

a property on an object and can be used as a type guard, and the TypeScript can know which type you used.

All 12 comments

The doc says :

To get the same code working, we’ll need to use a type assertion:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

http://www.typescriptlang.org/docs/handbook/advanced-types.html

Then in your sample:

if ((<A>AorB).a) {
    // use AorB.a
}

The issue here is that because B doesn't declare an a property, it might at run-time have an a property of any possible type (because you can assign an object with any set of properties to a B as long as it has a b property of type string). You can make it work explicitly declaring an a: undefined property in B (thus ensuring that B won't have some random a property):

type A = { a: string; } 
type B = { b: string; a: undefined }
type AorB = A | B;

declare const AorB: AorB;

if (AorB.a) {
    // Ok
}

Makes perfect sense. Brain fart.

If you define a: undefined on type B, you'll have to set it to undefined when you create/pass a variable.
To get around that, you can declare a as a?: undefined, and typescript will be happy if you omit it.

If I understand the comments correctly, this should work (but throws; tested on playground): Is this a bug or not? 🤔

type LinkProps = {
    to: string;
    onClick?: undefined;
    // Link specific props:
    target: string;
}

type ButtonProps = {
    to?: undefined;
    onClick: Function;
    // Button specific props:
    disabled: boolean;
}

type ActionProps = LinkProps | ButtonProps;

const Action = (props: ActionProps) =>
    props.to ?
        'Link with target: ' + props.target // Error not on ButtonProps
    :
        'Button with disabled: ' + props.disabled; // Error: not on LinkProps

Action({
  to: 'dssd',
  target: '_blank'
});

I don't understand this at all. Isn't if ((<A>AorB).a) the same as using if (A.a) because you're forcing it back to type A?

If you define a: undefined on type B, you'll have to set it to undefined when you create/pass a variable.
To get around that, you can declare a as a?: undefined, and typescript will be happy if you omit it.

might be better a?: never, now you can not assign undefined by mistake

What if you don't give a name to each of the members of the union? (can't make a type assertion as above without a name for the asserted type, can I?)

type u = "str" | {prop:"val"};
function f(arg:u){return arg.prop} // TypeScript: Property 'prop' does not exist on type 'u'

Any solution?

i find a solution on book :

interface A { x:  number;} 
interface B { y:  string;}
function doStuff ( q: A | B ) {
  if ( 'x'  in q) {
    //  if type A...
  }
  else {
    // if type B...
  }
}

Excerpt From: Basarat Ali Syed. “TypeScript Deep Dive.” Apple Books.

a property on an object and can be used as a type guard, and the TypeScript can know which type you used.

type ColorItemType = {
    colorId: number,
    colorName: string,
}

type NumItemType = {
    numId: number,
    numName: string
}

type ResType = {
    itemId: number,
    // 0 color 1 num
    type: number,
    itemInfo: {
        colorList: Array<ColorItemType>
        numList: Array<NumItemType>
    }
}

const request = () => {
    return [{
        itemId: 1,
        type: 0,
        itemInfo: {
            colorList: [{
                colorId: 1,
                colorName: 'blue'
            }],
            numList: []
        }
    }];
};

const dataSource: Array<ResType> = request();

const formatData = dataSource.map(dataItem => {
    const list: Array<ColorItemType | NumItemType> = dataItem.type === 1 ? dataItem.itemInfo.numList : dataItem.itemInfo.colorList;
    return list.map(listItem => {
        return {
            // An error will be reported here
            value: listItem.numId || listItem.colorId,
            label: listItem.numName || listItem.colorName
        };
    });
});

The issue here is that because B doesn't declare an a property, it might at run-time have an a property of any possible type (because you can assign an object with any set of properties to a B as long as it has a b property of type string). You can make it work explicitly declaring an a: undefined property in B (thus ensuring that B won't have some random a property):

type A = { a: string; } 
type B = { b: string; a: undefined }
type AorB = A | B;

declare const AorB: AorB;

if (AorB.a) {
    // Ok
}

Does not work for me 07.10.2020

And I think that this behavior of TS is not good because it does not help a developer much...

Was this page helpful?
5 / 5 - 1 ratings