Alex Ironside
Alex Ironside

Reputation: 5059

Casting types in a switch

I am trying to modularize my type casting. This is my code:

export enum detailsDataTypes {
  MACHINE = 'MACHINE',
  USER = 'USER',
  ABSTRACT = 'ABSTRACT',
}

export type sharedTypes = {
  name?: string;
  OEECategory?: string;
  OAECategory?: string;
  color?: string;
  type?: detailsDataTypes;
};

export type AbstractData = sharedTypes & {
  layer: string;
};

export type UserData = sharedTypes & {
  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
};

export type MachineData = sharedTypes & {
  stateCategory: string;
  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
  canOverrideOnlyByStates: boolean;
};

type DetailsData = AbstractData | UserData | MachineData;

export const detailsDataCaster = (data: DetailsData): DetailsData | null => {
  switch (data.type) {
    case detailsDataTypes.ABSTRACT:
      return <AbstractData>data;
    case detailsDataTypes.MACHINE:
      return <MachineData>data;
    case detailsDataTypes.USER:
      return <UserData>data;
    default:
      console.log('Wrong data type. Go yell at backend team or check your mocks');
      return null;
  }
};

What I'm trying to achieve, is tell TS that I'm returning AbstractData OR UserData OR MachineData OR null. But now, it doesn't like me doing this:

export const Details: FC<IDetails> = ({ data: $data = mockUserData }): ReactElement => {
  const data = detailsDataCaster($data);
  switch (data.type) {
    case detailsDataTypes.ABSTRACT:
      return <>Abstract</>;
    case detailsDataTypes.MACHINE:
      return <>Machine</>;
    case detailsDataTypes.USER:
      return <UserDisplay data={data} />; // error is here
    default:
      return <div />;
  }
};

Error

Type 'DetailsData' is not assignable to type 'UserData'.
  Type 'AbstractData' is not assignable to type 'UserData'.
    Type 'AbstractData' is missing the following properties from type '{ timeout: number; stateAfterTimeout: string; accessCanChoose: string; accessCanOverride: string; }': timeout, stateAfterTimeout, accessCanChoose, accessCanOverride

As I understand | does not mean OR in TS. I know there is the AND &, but this would make no sense. I thought that if I cast, it would be clear as to what I'm returning, but I see it's not so simple. What am I missing?

Upvotes: 1

Views: 208

Answers (2)

T.D. Stoneheart
T.D. Stoneheart

Reputation: 901

You might want to add a discriminant value to each type like this...

export type AbstractData = sharedTypes & {
  layer: string;
  type: detailsDataTypes.ABSTRACT; // add this field, it can only be of this literal value
};

export type UserData = sharedTypes & {
  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
  type: detailsDataTypes.USER; // add this field
};

export type MachineData = sharedTypes & {
  stateCategory: string;
  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
  canOverrideOnlyByStates: boolean;
  type: detailsDataTypes.MACHINE; // add this field
};

type DetailsData = AbstractData | UserData | MachineData;
function Details(data: DetailsData) {
    switch (data.type) {
        case detailsDataTypes.ABSTRACT:
            data; // data is AbstractData
            break;
            
        case detailsDataTypes.MACHINE:
            data; // data is MachineData
            break;

        case detailsDataTypes.USER:
            data; // data is UserData
            break;

        default:
            data; // data is never (no more possibilities left!)
            break;
    }
}

This technique is called discriminated unions.

Upvotes: 1

Akxe
Akxe

Reputation: 11545

If every type extending the sahredTypes would specify its own type the switch will magically work:

export enum detailsDataTypes {
  MACHINE = 'MACHINE',
  USER = 'USER',
  ABSTRACT = 'ABSTRACT',
}

export interface sharedTypes {
  name?: string;
  OEECategory?: string;
  OAECategory?: string;
  color?: string;
  type?: detailsDataTypes;
};

export interface AbstractData extends sharedTypes {
  type: detailsDataTypes.ABSTRACT;

  layer: string;
};

export interface UserData extends sharedTypes {
  type: detailsDataTypes.USER;

  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
};

export interface MachineData extends sharedTypes {
  type: detailsDataTypes.MACHINE;

  stateCategory: string;
  timeout: number;
  stateAfterTimeout: string;
  accessCanChoose: string;
  accessCanOverride: string;
  canOverrideOnlyByStates: boolean;
};

type DetailsData = AbstractData | UserData | MachineData;

Playgound link that shows the type is correctly inferred from the switch.

Upvotes: 2

Related Questions