0

I have an interface

 interface Test {
   property: SpecialProperty;
 }

now SpecialProperty can be ONE of each :


export type SpecialProperty= {
      withPropA: PropA;
    }
  | {
      withPropB: PropB;
    }
  | {
      withPropC: PropC;
    }

there can't be withPropA and another property etc..

The problem is the compiler do not allow me to do this.

if I do


function(param: Test ) {
 if(param.property.withPropA) {
   const prop:PropA = param.property.withPropA;
 }
 if(param.property.withPropB) {
   const prop:PropB = param.property.withPropB;
 }

}

the compiler say

Property 'withPropA' does not exist on type '{ withPropB: PropB; }'.

I tried casting etc... but nothing

Bobby
  • 4,372
  • 8
  • 47
  • 103
  • 1
    The issue isn't the "one of" part, it's accessing a property that doesn't exist on every member of the union. You can do `if("withPropA" in param.property) {` which is a type guard: https://www.typescriptlang.org/docs/handbook/2/narrowing.html – Linda Paiste Mar 06 '21 at 10:23
  • 1
    @LindaPaiste - Sadly, that page doesn't (any longer?) show this kind of check (lots of others, but not this kind). I haven't found the page that does (now). :-| Certainly they *should* be on that page... – T.J. Crowder Mar 06 '21 at 10:27
  • 2
    @T.J.Crowder I know! I went to pull up the old link and saw the big "deprecated" banner but the old page had more sections on it. Didn't want to link to a deprecated page but..."Using the `in` Operator": https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator – Linda Paiste Mar 06 '21 at 10:29
  • @LindaPaiste - Doh! Why didn't I just link to the deprecated page?! :-) I'm not 100% today, clearly... – T.J. Crowder Mar 06 '21 at 10:35

2 Answers2

3

You need to use a property existence check to narrow the type of param.property rather than a truthiness check:

function example(param: Test ) {
    if ("withPropA" in param.property) {                  // ***
        const prop:PropA = param.property.withPropA;
    }
    if ("withPropB" in param.property) {                  // ***
        const prop:PropB = param.property.withPropB;
    }
}

Playground link

Your truthiness check required that you read the value of the property, but TypeScript doesn't know whether the object has that property when you ask it to do that.

More here. That page is supposedly deprecated in favor of this page now, but the new page doesn't cover this particular check (yet, the new page is brand new).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

To go with union types, you should declare some common property for all unions.

For example type or kind.

See next example:

interface Test {
    property: SpecialProperty;
}

type PropA = 'a'
type PropB = 'b'
type PropC = 'c'


//https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
export type SpecialProperty =
    | {
        type: 'A',
        withPropA: PropA;
    }
    | {
        type: 'B',
        withPropB: PropB;
    }
    | {
        type: 'C'
        withPropC: PropC;
    }


function test(param: Test) {
    if (param.property.type === 'A') {
        const prop: PropA = param.property.withPropA;
    }
    if (param.property.type === 'B') {
        const prop: PropB = param.property.withPropB;
    }

}

Because this is how TypeScript discrimunated unions works

Playground

I know, I know, now you might say to me:

Wait, I can't change my SpecialProperty, because this type is from third party library, I don't have control over this type.

In this case you can use next utils:


type PropA = 'a'
type PropB = 'b'
type PropC = 'c'


export type SpecialProperty =
    | {

        withPropA: PropA;
    }
    | {

        withPropB: PropB;
    }
    | {

        withPropC: PropC;
    }
    
// credits goes to https://stackoverflow.com/questions/65805600/struggling-with-building-a-type-in-ts#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>
type StrictUnionWrapper = StrictUnion<SpecialProperty>

interface Test {
    property: StrictUnionWrapper;
}

function test(param: Test) {
    if (param.property.withPropA) {
        const prop: PropA = param.property.withPropA;
    }
    if (param.property.withPropB) {
        const prop: PropB = param.property.withPropB;
    }

}

Playground

Personally, I use next typeguard for such kind of check:

const hasProperty = <T, U extends string>(obj: T, prop: U): obj is T & Record<U, unknown> =>
    Object.prototype.hasOwnProperty.call(obj, prop);

const foo = (arg: unknown) => {
    if (hasProperty(arg, 'age')) {
        const b = arg // Record<"age", unknown>
        b.age // ok
    }
}

Playground