Is it possible to do conditional destructuring of a union type in typescript?

65 Views Asked by At

I have a React application (although the question is not really about React), with typescript.

In a functional component (whose consumer I want to be able to pass OR styles OR a class, but not both), I use the following props type:

type AnimationContainerProps = { hidingTimeoutMs: number } & (
    | {
        displayingClass: string;
        hidingClass: string;
      }
    | {
        displayingInlineStyle: string;
        hidingInlineStyle: string;
      }
  );

I would like to destructure it in a single instruction, like:

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props

In javascript, it would be no problem and I would receive undefined in the non-existent props. But in Typescript, I can't do this, because I'll get a Property 'XXX' does not exist on type 'AnimationContainerProps'.ts(2339).

I want to avoid making the attributions one by one. Do you guys think of a way to destructure it?

4

There are 4 best solutions below

2
Michael Liu On

Here's one way you could do it:

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = {
    displayingClass: undefined,
    hidingClass: undefined,
    displayingInlineStyle: undefined,
    hidingInlineStyle: undefined,
    ...props
};

For the missing properties, you can use undefined or any other value or object that's convenient.


In a comment, jcalz points out that your definition of the AnimationContainerProps type is not properly exclusive. For example, someone could pass string values for all four class/style props to your component. Or they could pass the following object, which has displayingInlineStyle and hidingInlineStyle values of the wrong type:

const props = {
    hidingTimeoutMs: 0,
    displayingClass: "",
    hidingClass: "",
    displayingInlineStyle: 0,
    hidingInlineStyle: false,
};

<AnimationContainer {...props} />

If you're concerned about this case, you can change your type and force missing/undefined values for displayingInlineStyle and hidingInlineStyle if displayingClass and hidingClass are specified, and vice versa:

type AnimationContainerProps = { hidingTimeoutMs: number } & (
    | {
        displayingClass: string;
        hidingClass: string;
        displayingInlineStyle?: undefined;
        hidingInlineStyle?: undefined;
      }
    | {
        displayingClass?: undefined;
        hidingClass?: undefined;
        displayingInlineStyle: string;
        hidingInlineStyle: string;
      }
);

With this change, your original destructuring code would work as desired.

1
Riad Baghbanli On

TypeScript enforces type conformity, hence you have two options to retain type enforcement.

Option 1 - supercomposition:

type AnimationContainerProps = { hidingTimeoutMs: number } & { option: (
  | {
    displayingClass: string;
    hidingClass: string;
    }
  | {
    displayingInlineStyle: string;
    hidingInlineStyle: string;
    }
) };

let props = {} as AnimationContainerProps;
const { hidingTimeoutMs, option } = props;

Option 2 - decomposition:

type AnimationContainerProps = {
  hidingTimeoutMs: number;
  displayingClass: string | undefined;
  hidingClass: string | undefined;
  displayingInlineStyle: string | undefined;
  hidingInlineStyle: string | undefined;
};

let props = {} as AnimationContainerProps;

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props;
2
Abdullah Ch On

You could use a typeGuard function to decide what to destructure from your props (inline or class)

// Type guard function
const hasInlineStyles = (
  props: AnimationContainerProps
): props is {
  displayingInlineStyle: string;
  hidingInlineStyle: string;
  hidingTimeoutMs: number;
} => 'displayingInlineStyle' in props;

// Example usage in your component
const Component = (props: AnimationContainerProps) => {
  const { hidingTimeoutMs } = props;
  let displayingStyle: string;
  let hidingStyle: string;

  if (hasInlineStyles(props)) {
    displayingStyle = props.displayingInlineStyle;
    hidingStyle = props.hidingInlineStyle;
    // Use displayingStyle and hidingStyle
  } else {
    displayingStyle = props.displayingClass;
    hidingStyle = props.hidingClass;
    // Use displayingStyle and hidingStyle
  }

  // Rest of your component logic
};

0
captain-yossarian from Ukraine On

Here you will find a nice utility for such case:


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;

// taken from here https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnion<T> = StrictUnionHelper<T, T>

type AnimationContainerProps = { hidingTimeoutMs: number } & StrictUnion<(
    | {
        displayingClass: string;
        hidingClass: string;
    }
    | {
        displayingInlineStyle: string;
        hidingInlineStyle: string;
    }
)>;

declare let props:AnimationContainerProps

// ok
const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props