2

I've searched trough the stackoverflow questions but didn't find the needed answer in my case.
I have

interface FruitBox {
  name: string
  desc: {
   'orange': number;
   'banana': number;
  }
}

interface IceBox {
  name: string
}

interface Vegabox {
  name: string
  desc: {
   'tomato': number;
   'potato': number;
  }
}

type UnionBox = FruitBox | Vegabox | IceBox;

What I'm trying to implement

type Ship = Record<string, (description: UnionBox['desc'] | UnionBox['name']) => void>;

I'm trying to assign a description property to a type of UnionBox['desc'] and if desc parameter doesn't exist, then assign it to the UnionBox['name']

Receiving next error: Property 'desc' does not exist on type 'UnionBox'

Would be glad for any help or hint

ps. I understand that there must be some weird algorithm, Just not sure in which way to look

Sergii Onish
  • 140
  • 1
  • 8

2 Answers2

1

See this answer. The case is very similar.

When TypeScript resolves union type it allows you to use best common type. I mean the type which is common for all union elements. Because this option is the safest one.

Let's inspect your union:

interface FruitBox {
  name: string
  desc: {
   'orange': number;
   'banana': number;
  }
}

interface IceBox {
  name: string
}

interface Vegabox {
  name: string
  desc: {
   'tomato': number;
   'potato': number;
  }
}

type UnionBox = FruitBox | Vegabox | IceBox;

type AllowedKeys = keyof UnionBox // name

As you might have noticed (AllowedKeys) you are only allowed to use name property from the UnionBox. Why ? Because name exists in each union.

In order to handle it, you might want to use StrictUnion helper.

// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#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>

Full code:

interface FruitBox {
    name: string
    desc: {
        'orange': number;
        'banana': number;
    }
}

interface IceBox {
    name: string
}

interface Vegabox {
    name: string
    desc: {
        'tomato': number;
        'potato': number;
    }
}

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 UnionBox = StrictUnion<FruitBox | Vegabox | IceBox>;

type AllowedKeys = keyof UnionBox // name


type Ship = Record<string, (description: UnionBox['desc'] | UnionBox['name']) => void>; // ok


const record: Ship = {
    foo: (elem) => { }
}

Playground

0

UnionBox from your example can be one of the types - FruitBox, Vegabox, or IceBox. IceBox is the one that makes Typescript unhappy - it doesn't contain a property desc. Imagine having an object of type IceBox, then passing it to a function, which takes UnionBox as a parameter and then uses the property desc - it would access a non-existing one.

I'm trying to assign a description property to a type of UnionBox['desc'] and if desc parameter doesn't exist, then assign it to the UnionBox['name']

I'm not sure how you expect it to work, since the description is simply typed as either string or string in your example - | operator will simply create a union. But I guess you could simply accept unionBox: UnionBox as a parameter, and add a logic inside which would have unionBox.desc ?? unionBox.name to get name if desc does not exist.

Vladyslav Zavalykhatko
  • 15,202
  • 8
  • 65
  • 100