Crocsx
Crocsx

Reputation: 7609

type that can be ONE of multiple key typescript

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

Upvotes: 0

Views: 61

Answers (2)

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

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074188

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).

Upvotes: 3

Related Questions