1

In this TypeScript example

type SomeType = "1" | "2" | "3" | "4" | "5" | "6" | "7";
type SomeStatus = "one" | "two" | "three" | "four" | "five" | "six" | "seven";

type TypeDiffer = {
    type: "1"
} | {
    type: Exclude<SomeType, "1">
}

type StatusDiffer = {
    status: "one"
} | {
    status: Exclude<SomeStatus, "one">
}

type OrderStatusFull = StatusDiffer & TypeDiffer;

const apiResponse: any = {};

const example: OrderStatusFull = {
    type: apiResponse.type as SomeType,
    status: apiResponse.status as SomeStatus
}

I've got the Typescript error:

Type '{ type: SomeType; status: SomeStatus; }' is not assignable to type 'OrderStatusFull'.
  Type '{ type: SomeType; status: SomeStatus; }' is not assignable to type '{ status: "two" | "three" | "four" | "five" | "six" | "seven"; } & { type: "2" | "3" | "4" | "5" | "6" | "7"; } & { status: SomeStatus; }'.
    Type '{ type: SomeType; status: SomeStatus; }' is not assignable to type '{ status: "two" | "three" | "four" | "five" | "six" | "seven"; }'.
      Types of property 'status' are incompatible.
        Type 'SomeStatus' is not assignable to type '"two" | "three" | "four" | "five" | "six" | "seven"'.
          Type '"one"' is not assignable to type '"two" | "three" | "four" | "five" | "six" | "seven"'.

When I reduce number of types and statuses to 5 (or 3 and 7) it works. It works if multiplied number of items is up to 25.

type SomeType = "1" | "2" | "3" | "4" | "5"; // | "6" | "7";
type SomeStatus = "one" | "two" | "three" | "four" | "five"; // | "six" | "seven";

...

It also works with full list of types and statuses when I remove as SomeStatus, but when I define apiResponse more precisely, the problem is back.

const apiResponse: {
    type: SomeType,
    status: SomeStatus
} = {
    type: "1",
    status: "one"
}

const example: OrderStatusFull = {
    type: apiResponse.type,
    status: apiResponse.status
}

The only workaround is force type's or status's type to subset of the type, like this:

const example: OrderStatusFull = {
    type: apiResponse.type as "1" | "2",
    status: apiResponse.status
}

Can anyone explain why this happen and how to use type checking without workarounds?

The real world scenario is for example stock exchange api response, when based on order status and type combination different properties are filled.

Update: I've made stock exchange example in Playground. I omit the problem here, just to show which use cases I'm trying with union types in real life situations.

Boris Šuška
  • 1,796
  • 20
  • 32
  • 1
    [TS3.5 introduced additional union checking](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking) in [ms/TS#30779](https://github.com/microsoft/TypeScript/pull/30779) that compares two types by propagating unions up out of properties, but in general this would be a disastrously slow thing to do, so there is a hardcoded limit at 25 union members. That's the "why". When you say "use type checking without workarounds" I'm not sure what you're looking for; isn't any sort of refactoring you do here going to be a "workaround"? – jcalz Feb 21 '22 at 15:48
  • I'd be happy to write up an answer explaining why this happens, but I think you might want to be more explicit about what you consider a workaround and what you consider a non-workaround solution. – jcalz Feb 21 '22 at 15:49
  • 1
    `Order` type does not look in a way as you expect after intersection. Try to Debug it. `type Debug={[P in keyof Order]: Order[P]}` – captain-yossarian from Ukraine Feb 21 '22 at 18:17
  • @jcalz, I've updated my question with link to example of using stock exchange types and functions implementing some Use cases. Actually types definition should change to make it work, it's not a workaround it's solution for me. What I called workaround in original question is use types like `"1" | "2"` even I know that it is `SomeType` in fact. I used `"1" | "2"` as ugly workaround just to be under limit of 25 combinations. – Boris Šuška Feb 21 '22 at 22:41
  • @captain-yossarian, awesome trick with the `Debug` type! Yes, it's something different than I expect. Seems like I understand union and intersection types wrongly. – Boris Šuška Feb 21 '22 at 23:09
  • Hmm, the playground link is giving you a problem because `Omit` isn't distributive across unions in `T`, see [this q/a](https://stackoverflow.com/questions/57103834/typescript-omit-a-property-from-all-interfaces-in-a-union-but-keep-the-union-s) for a fix, which looks like [this](https://tsplay.dev/N7PvDm). I'm confused because the issue there is completely different from the 25-member-limit issue from your main question. What should I be doing here? How shall I proceed? – jcalz Feb 21 '22 at 23:48

1 Answers1

1

In this case it worth using discriminated unions:

type SomeType = "1" | "2" | "3" | "4" | "5" | "6" | "7";
type SomeStatus = "one" | "two" | "three" | "four" | "five" | "six" | "seven";

interface Base {
    status: SomeStatus,
    type: SomeType
}

interface OrderStatusWithOne extends Base {
    status: 'one',
    type: '1'
}

interface OrderStatusWithoutOne extends Base {
    status: Exclude<SomeStatus, "one">,
    type: Exclude<SomeType, "1">
}

type OrderStatusFull = OrderStatusWithOne | OrderStatusWithoutOne

declare let apiResponse: OrderStatusFull


const example: OrderStatusFull = {
    type: apiResponse.type, // type is  "1" | "2" | "3" | "4" | "5" | "6" | "7"
    status: apiResponse.status // status is  "one" | "two" | "three" | "four" | "five" | "six" | "seven"
}

Playground

However there is still an error because type property of apiResponse is in fact a union of all possible types. The problem is in destructuring. You are not allowed to use OrderStatusFull for destructured object of this type. Again, type is a union of 1 and Exclude<SomeType, "1"> - it means that it does not meet requirements in OrderStatusFull union.

In order to make it a bit safer, you can use function:

const handle = ({ type, status }: OrderStatusFull): OrderStatusFull =>
    type === '1'
        ? { type, status }
        : { type, status };

I know, it looks weird but it works. If you don't like extra function approach I think it will be justified if you use type assertion in this case.