Search code examples
typescripttypescript-genericsmapped-types

Interface [key in StringEnum] when not all elements of StringEnum will exist as keys?


Consider the following example data representing shops containing lists of pending and fulfilled purchase order ids:

{
    "shop-45": {
        "FULFILLED": [55, 70]
    },
    "shop-46: {
        "PENDING": [59, 54]
        "FULFILLED": [100, 101]
    }
}

If a shop has neither pending nor fulfilled orders, it does not appear in the list at all.

I attempted to represent this using the following:

type Status = "PENDING" | "FULFILLED";

interface ShopList {
    [shop: string]: {
        [status in Status]: number[];
    }
}

Unsurprisingly, tsc complains when I do not have both PENDING and FULFILLED as sub-properties of a shop.

I quelled the error by making that property optional ([status in Status]?: number[]) but I don't think that's really what I want to do, since a shop will never have zero sub-properties.

Another whimsical, shot-in-the-dark attempt [status in Partial<Status>]: number[]; complains similarly.

Is that my only option, and simply not something to worry about?

This is a trivialized MCVE; the real one is far more complex and has more layers. That's (always?) the motivation behind using generics rather than repeating every possible key name: the enum is used as field values in other objects.

TypeScript/issues references discussing similar-seeming situations: 7374 | 19211 | 14934 | 5683.


Solution

  • If you're looking to bar the scenario where a shop won't show up without either PENDING or FULFILLED you will need to be a little more explicit with your types. For instance, you could do the following:

    type ShopWithPending = {
        PENDING: number[];
    }
    
    type ShopWithFulfilled = {
        FULFILLED: number[];
    }
    
    type ShopStatus = ShopWithPending | ShopWithFulfilled | (ShopWithPending & ShopWithFulfilled);
    
    interface ShopList {
        [shop: string]: ShopStatus
    }
    

    However, this does make it hard to then use shops pulled out of the list, since Typescript will see the ShopStatus type as something that is not guaranteed to have any properties and will thus not allow you to deference PENDING or FULFILLED.

    In order to get back that ability, you will need something else on the Shop types that allow Typescript to narrow back down the type inference to a specific typed version.